mirror of
https://github.com/qbittorrent/qBittorrent.git
synced 2025-12-22 08:27:24 -06:00
Merge pull request #15793 from glassez/save-path
Redesign "Incomplete folder" feature
This commit is contained in:
@@ -8,6 +8,7 @@ add_library(qbt_base STATIC
|
||||
bittorrent/bandwidthscheduler.h
|
||||
bittorrent/bencoderesumedatastorage.h
|
||||
bittorrent/cachestatus.h
|
||||
bittorrent/categoryoptions.h
|
||||
bittorrent/common.h
|
||||
bittorrent/customstorage.h
|
||||
bittorrent/dbresumedatastorage.h
|
||||
@@ -100,6 +101,7 @@ add_library(qbt_base STATIC
|
||||
bittorrent/abstractfilestorage.cpp
|
||||
bittorrent/bandwidthscheduler.cpp
|
||||
bittorrent/bencoderesumedatastorage.cpp
|
||||
bittorrent/categoryoptions.cpp
|
||||
bittorrent/customstorage.cpp
|
||||
bittorrent/dbresumedatastorage.cpp
|
||||
bittorrent/downloadpriority.cpp
|
||||
|
||||
@@ -7,6 +7,7 @@ HEADERS += \
|
||||
$$PWD/bittorrent/bandwidthscheduler.h \
|
||||
$$PWD/bittorrent/bencoderesumedatastorage.h \
|
||||
$$PWD/bittorrent/cachestatus.h \
|
||||
$$PWD/bittorrent/categoryoptions.h \
|
||||
$$PWD/bittorrent/common.h \
|
||||
$$PWD/bittorrent/customstorage.h \
|
||||
$$PWD/bittorrent/downloadpriority.h \
|
||||
@@ -100,6 +101,7 @@ SOURCES += \
|
||||
$$PWD/bittorrent/abstractfilestorage.cpp \
|
||||
$$PWD/bittorrent/bandwidthscheduler.cpp \
|
||||
$$PWD/bittorrent/bencoderesumedatastorage.cpp \
|
||||
$$PWD/bittorrent/categoryoptions.cpp \
|
||||
$$PWD/bittorrent/customstorage.cpp \
|
||||
$$PWD/bittorrent/dbresumedatastorage.cpp \
|
||||
$$PWD/bittorrent/downloadpriority.cpp \
|
||||
|
||||
@@ -48,7 +48,8 @@ namespace BitTorrent
|
||||
QString category;
|
||||
TagSet tags;
|
||||
QString savePath;
|
||||
bool disableTempPath = false; // e.g. for imported torrents
|
||||
std::optional<bool> useDownloadPath;
|
||||
QString downloadPath;
|
||||
bool sequential = false;
|
||||
bool firstLastPiecePriority = false;
|
||||
bool addForced = false;
|
||||
|
||||
@@ -173,12 +173,19 @@ std::optional<BitTorrent::LoadTorrentParams> BitTorrent::BencodeResumeDataStorag
|
||||
torrentParams.restored = true;
|
||||
torrentParams.category = fromLTString(root.dict_find_string_value("qBt-category"));
|
||||
torrentParams.name = fromLTString(root.dict_find_string_value("qBt-name"));
|
||||
torrentParams.savePath = Profile::instance()->fromPortablePath(
|
||||
Utils::Fs::toUniformPath(fromLTString(root.dict_find_string_value("qBt-savePath"))));
|
||||
torrentParams.hasSeedStatus = root.dict_find_int_value("qBt-seedStatus");
|
||||
torrentParams.firstLastPiecePriority = root.dict_find_int_value("qBt-firstLastPiecePriority");
|
||||
torrentParams.seedingTimeLimit = root.dict_find_int_value("qBt-seedingTimeLimit", Torrent::USE_GLOBAL_SEEDING_TIME);
|
||||
|
||||
torrentParams.savePath = Profile::instance()->fromPortablePath(
|
||||
Utils::Fs::toUniformPath(fromLTString(root.dict_find_string_value("qBt-savePath"))));
|
||||
torrentParams.useAutoTMM = torrentParams.savePath.isEmpty();
|
||||
if (!torrentParams.useAutoTMM)
|
||||
{
|
||||
torrentParams.downloadPath = Profile::instance()->fromPortablePath(
|
||||
Utils::Fs::toUniformPath(fromLTString(root.dict_find_string_value("qBt-downloadPath"))));
|
||||
}
|
||||
|
||||
// TODO: The following code is deprecated. Replace with the commented one after several releases in 4.4.x.
|
||||
// === BEGIN DEPRECATED CODE === //
|
||||
const lt::bdecode_node contentLayoutNode = root.dict_find("qBt-contentLayout");
|
||||
@@ -352,7 +359,6 @@ void BitTorrent::BencodeResumeDataStorage::Worker::store(const TorrentID &id, co
|
||||
}
|
||||
}
|
||||
|
||||
data["qBt-savePath"] = Profile::instance()->toPortablePath(resumeData.savePath).toStdString();
|
||||
data["qBt-ratioLimit"] = static_cast<int>(resumeData.ratioLimit * 1000);
|
||||
data["qBt-seedingTimeLimit"] = resumeData.seedingTimeLimit;
|
||||
data["qBt-category"] = resumeData.category.toStdString();
|
||||
@@ -362,6 +368,12 @@ void BitTorrent::BencodeResumeDataStorage::Worker::store(const TorrentID &id, co
|
||||
data["qBt-contentLayout"] = Utils::String::fromEnum(resumeData.contentLayout).toStdString();
|
||||
data["qBt-firstLastPiecePriority"] = resumeData.firstLastPiecePriority;
|
||||
|
||||
if (!resumeData.useAutoTMM)
|
||||
{
|
||||
data["qBt-savePath"] = Profile::instance()->toPortablePath(resumeData.savePath).toStdString();
|
||||
data["qBt-downloadPath"] = Profile::instance()->toPortablePath(resumeData.downloadPath).toStdString();
|
||||
}
|
||||
|
||||
const QString resumeFilepath = m_resumeDataDir.absoluteFilePath(QString::fromLatin1("%1.fastresume").arg(id.toString()));
|
||||
const nonstd::expected<void, QString> result = Utils::IO::saveToFile(resumeFilepath, data);
|
||||
if (!result)
|
||||
|
||||
78
src/base/bittorrent/categoryoptions.cpp
Normal file
78
src/base/bittorrent/categoryoptions.cpp
Normal file
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2021 Vladimir Golovnev <glassez@yandex.ru>
|
||||
*
|
||||
* 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 "categoryoptions.h"
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
|
||||
const QString OPTION_SAVEPATH {QStringLiteral("save_path")};
|
||||
const QString OPTION_DOWNLOADPATH {QStringLiteral("download_path")};
|
||||
|
||||
BitTorrent::CategoryOptions BitTorrent::CategoryOptions::fromJSON(const QJsonObject &jsonObj)
|
||||
{
|
||||
CategoryOptions options;
|
||||
options.savePath = jsonObj.value(OPTION_SAVEPATH).toString();
|
||||
|
||||
const QJsonValue downloadPathValue = jsonObj.value(OPTION_DOWNLOADPATH);
|
||||
if (downloadPathValue.isBool())
|
||||
options.downloadPath = {downloadPathValue.toBool()};
|
||||
else if (downloadPathValue.isString())
|
||||
options.downloadPath = {true, downloadPathValue.toString()};
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
QJsonObject BitTorrent::CategoryOptions::toJSON() const
|
||||
{
|
||||
QJsonValue downloadPathValue = QJsonValue::Undefined;
|
||||
if (downloadPath)
|
||||
{
|
||||
if (downloadPath->enabled)
|
||||
downloadPathValue = downloadPath->path;
|
||||
else
|
||||
downloadPathValue = false;
|
||||
}
|
||||
|
||||
return {
|
||||
{OPTION_SAVEPATH, savePath},
|
||||
{OPTION_DOWNLOADPATH, downloadPathValue}
|
||||
};
|
||||
}
|
||||
|
||||
bool BitTorrent::operator==(const BitTorrent::CategoryOptions::DownloadPathOption &left, const BitTorrent::CategoryOptions::DownloadPathOption &right)
|
||||
{
|
||||
return ((left.enabled == right.enabled)
|
||||
&& (left.path == right.path));
|
||||
}
|
||||
|
||||
bool BitTorrent::operator==(const BitTorrent::CategoryOptions &left, const BitTorrent::CategoryOptions &right)
|
||||
{
|
||||
return ((left.savePath == right.savePath)
|
||||
&& (left.downloadPath == right.downloadPath));
|
||||
}
|
||||
56
src/base/bittorrent/categoryoptions.h
Normal file
56
src/base/bittorrent/categoryoptions.h
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2021 Vladimir Golovnev <glassez@yandex.ru>
|
||||
*
|
||||
* 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 <optional>
|
||||
|
||||
#include <QString>
|
||||
|
||||
class QJsonObject;
|
||||
|
||||
namespace BitTorrent
|
||||
{
|
||||
struct CategoryOptions
|
||||
{
|
||||
struct DownloadPathOption
|
||||
{
|
||||
bool enabled;
|
||||
QString path;
|
||||
};
|
||||
|
||||
QString savePath;
|
||||
std::optional<DownloadPathOption> downloadPath;
|
||||
|
||||
static CategoryOptions fromJSON(const QJsonObject &jsonObj);
|
||||
QJsonObject toJSON() const;
|
||||
};
|
||||
|
||||
bool operator==(const CategoryOptions::DownloadPathOption &left, const CategoryOptions::DownloadPathOption &right);
|
||||
bool operator==(const CategoryOptions &left, const CategoryOptions &right);
|
||||
}
|
||||
@@ -28,6 +28,8 @@
|
||||
|
||||
#include "dbresumedatastorage.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <libtorrent/bdecode.hpp>
|
||||
#include <libtorrent/bencode.hpp>
|
||||
#include <libtorrent/entry.hpp>
|
||||
@@ -57,11 +59,13 @@ namespace
|
||||
{
|
||||
const char DB_CONNECTION_NAME[] = "ResumeDataStorage";
|
||||
|
||||
const int DB_VERSION = 1;
|
||||
const int DB_VERSION = 2;
|
||||
|
||||
const char DB_TABLE_META[] = "meta";
|
||||
const char DB_TABLE_TORRENTS[] = "torrents";
|
||||
|
||||
const char META_VERSION[] = "version";
|
||||
|
||||
struct Column
|
||||
{
|
||||
QString name;
|
||||
@@ -80,6 +84,7 @@ namespace
|
||||
const Column DB_COLUMN_CATEGORY = makeColumn("category");
|
||||
const Column DB_COLUMN_TAGS = makeColumn("tags");
|
||||
const Column DB_COLUMN_TARGET_SAVE_PATH = makeColumn("target_save_path");
|
||||
const Column DB_COLUMN_DOWNLOAD_PATH = makeColumn("download_path");
|
||||
const Column DB_COLUMN_CONTENT_LAYOUT = makeColumn("content_layout");
|
||||
const Column DB_COLUMN_RATIO_LIMIT = makeColumn("ratio_limit");
|
||||
const Column DB_COLUMN_SEEDING_TIME_LIMIT = makeColumn("seeding_time_limit");
|
||||
@@ -109,42 +114,50 @@ namespace
|
||||
return QString::fromLatin1("CREATE TABLE %1 (%2)").arg(quoted(tableName), items.join(QLatin1Char(',')));
|
||||
}
|
||||
|
||||
QString makeInsertStatement(const QString &tableName, const QVector<Column> &columns)
|
||||
std::pair<QString, QString> joinColumns(const QVector<Column> &columns)
|
||||
{
|
||||
QStringList names;
|
||||
names.reserve(columns.size());
|
||||
QStringList values;
|
||||
values.reserve(columns.size());
|
||||
int namesSize = columns.size();
|
||||
int valuesSize = columns.size();
|
||||
for (const Column &column : columns)
|
||||
{
|
||||
names.append(quoted(column.name));
|
||||
values.append(column.placeholder);
|
||||
namesSize += column.name.size() + 2;
|
||||
valuesSize += column.placeholder.size();
|
||||
}
|
||||
|
||||
const QString jointNames = names.join(QLatin1Char(','));
|
||||
const QString jointValues = values.join(QLatin1Char(','));
|
||||
QString names;
|
||||
names.reserve(namesSize);
|
||||
QString values;
|
||||
values.reserve(valuesSize);
|
||||
for (const Column &column : columns)
|
||||
{
|
||||
names.append(quoted(column.name) + QLatin1Char(','));
|
||||
values.append(column.placeholder + QLatin1Char(','));
|
||||
}
|
||||
names.chop(1);
|
||||
values.chop(1);
|
||||
|
||||
return std::make_pair(names, values);
|
||||
}
|
||||
|
||||
QString makeInsertStatement(const QString &tableName, const QVector<Column> &columns)
|
||||
{
|
||||
const auto [names, values] = joinColumns(columns);
|
||||
return QString::fromLatin1("INSERT INTO %1 (%2) VALUES (%3)")
|
||||
.arg(quoted(tableName), jointNames, jointValues);
|
||||
.arg(quoted(tableName), names, values);
|
||||
}
|
||||
|
||||
QString makeUpdateStatement(const QString &tableName, const QVector<Column> &columns)
|
||||
{
|
||||
const auto [names, values] = joinColumns(columns);
|
||||
return QString::fromLatin1("UPDATE %1 SET (%2) = (%3)")
|
||||
.arg(quoted(tableName), names, values);
|
||||
}
|
||||
|
||||
QString makeOnConflictUpdateStatement(const Column &constraint, const QVector<Column> &columns)
|
||||
{
|
||||
QStringList names;
|
||||
names.reserve(columns.size());
|
||||
QStringList values;
|
||||
values.reserve(columns.size());
|
||||
for (const Column &column : columns)
|
||||
{
|
||||
names.append(quoted(column.name));
|
||||
values.append(column.placeholder);
|
||||
}
|
||||
|
||||
const QString jointNames = names.join(QLatin1Char(','));
|
||||
const QString jointValues = values.join(QLatin1Char(','));
|
||||
|
||||
const auto [names, values] = joinColumns(columns);
|
||||
return QString::fromLatin1(" ON CONFLICT (%1) DO UPDATE SET (%2) = (%3)")
|
||||
.arg(quoted(constraint.name), jointNames, jointValues);
|
||||
.arg(quoted(constraint.name), names, values);
|
||||
}
|
||||
|
||||
QString makeColumnDefinition(const Column &column, const char *definition)
|
||||
@@ -187,7 +200,15 @@ BitTorrent::DBResumeDataStorage::DBResumeDataStorage(const QString &dbPath, QObj
|
||||
throw RuntimeError(db.lastError().text());
|
||||
|
||||
if (needCreateDB)
|
||||
{
|
||||
createDB();
|
||||
}
|
||||
else
|
||||
{
|
||||
const int dbVersion = currentDBVersion();
|
||||
if (dbVersion == 1)
|
||||
updateDBFromVersion1();
|
||||
}
|
||||
|
||||
m_asyncWorker = new Worker(dbPath, QLatin1String("ResumeDataStorageWorker"));
|
||||
m_asyncWorker->moveToThread(m_ioThread);
|
||||
@@ -276,8 +297,6 @@ std::optional<BitTorrent::LoadTorrentParams> BitTorrent::DBResumeDataStorage::lo
|
||||
const QStringList tagList = tagsData.split(QLatin1Char(','));
|
||||
resumeData.tags.insert(tagList.cbegin(), tagList.cend());
|
||||
}
|
||||
resumeData.savePath = Profile::instance()->fromPortablePath(
|
||||
Utils::Fs::toUniformPath(query.value(DB_COLUMN_TARGET_SAVE_PATH.name).toString()));
|
||||
resumeData.hasSeedStatus = query.value(DB_COLUMN_HAS_SEED_STATUS.name).toBool();
|
||||
resumeData.firstLastPiecePriority = query.value(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY.name).toBool();
|
||||
resumeData.ratioLimit = query.value(DB_COLUMN_RATIO_LIMIT.name).toInt() / 1000.0;
|
||||
@@ -288,6 +307,15 @@ std::optional<BitTorrent::LoadTorrentParams> BitTorrent::DBResumeDataStorage::lo
|
||||
query.value(DB_COLUMN_OPERATING_MODE.name).toString(), TorrentOperatingMode::AutoManaged);
|
||||
resumeData.stopped = query.value(DB_COLUMN_STOPPED.name).toBool();
|
||||
|
||||
resumeData.savePath = Profile::instance()->fromPortablePath(
|
||||
Utils::Fs::toUniformPath(query.value(DB_COLUMN_TARGET_SAVE_PATH.name).toString()));
|
||||
resumeData.useAutoTMM = resumeData.savePath.isEmpty();
|
||||
if (!resumeData.useAutoTMM)
|
||||
{
|
||||
resumeData.downloadPath = Profile::instance()->fromPortablePath(
|
||||
Utils::Fs::toUniformPath(query.value(DB_COLUMN_DOWNLOAD_PATH.name).toString()));
|
||||
}
|
||||
|
||||
const QByteArray bencodedResumeData = query.value(DB_COLUMN_RESUMEDATA.name).toByteArray();
|
||||
const QByteArray bencodedMetadata = query.value(DB_COLUMN_METADATA.name).toByteArray();
|
||||
const QByteArray allData = ((bencodedMetadata.isEmpty() || bencodedResumeData.isEmpty())
|
||||
@@ -297,6 +325,9 @@ std::optional<BitTorrent::LoadTorrentParams> BitTorrent::DBResumeDataStorage::lo
|
||||
lt::error_code ec;
|
||||
const lt::bdecode_node root = lt::bdecode(allData, ec);
|
||||
|
||||
resumeData.downloadPath = Profile::instance()->fromPortablePath(
|
||||
Utils::Fs::toUniformPath(fromLTString(root.dict_find_string_value("qBt-downloadPath"))));
|
||||
|
||||
lt::add_torrent_params &p = resumeData.ltAddTorrentParams;
|
||||
|
||||
p = lt::read_resume_data(root, ec);
|
||||
@@ -329,6 +360,33 @@ void BitTorrent::DBResumeDataStorage::storeQueue(const QVector<TorrentID> &queue
|
||||
});
|
||||
}
|
||||
|
||||
int BitTorrent::DBResumeDataStorage::currentDBVersion() const
|
||||
{
|
||||
const auto selectDBVersionStatement = QString::fromLatin1("SELECT %1 FROM %2 WHERE %3 = %4;")
|
||||
.arg(quoted(DB_COLUMN_VALUE.name), quoted(DB_TABLE_META), quoted(DB_COLUMN_NAME.name), DB_COLUMN_NAME.placeholder);
|
||||
|
||||
auto db = QSqlDatabase::database(DB_CONNECTION_NAME);
|
||||
QSqlQuery query {db};
|
||||
|
||||
if (!query.prepare(selectDBVersionStatement))
|
||||
throw RuntimeError(query.lastError().text());
|
||||
|
||||
query.bindValue(DB_COLUMN_NAME.placeholder, QString::fromLatin1(META_VERSION));
|
||||
|
||||
if (!query.exec())
|
||||
throw RuntimeError(query.lastError().text());
|
||||
|
||||
if (!query.next())
|
||||
throw RuntimeError(tr("Database is corrupted."));
|
||||
|
||||
bool ok;
|
||||
const int dbVersion = query.value(0).toInt(&ok);
|
||||
if (!ok)
|
||||
throw RuntimeError(tr("Database is corrupted."));
|
||||
|
||||
return dbVersion;
|
||||
}
|
||||
|
||||
void BitTorrent::DBResumeDataStorage::createDB() const
|
||||
{
|
||||
auto db = QSqlDatabase::database(DB_CONNECTION_NAME);
|
||||
@@ -353,7 +411,7 @@ void BitTorrent::DBResumeDataStorage::createDB() const
|
||||
if (!query.prepare(insertMetaVersionQuery))
|
||||
throw RuntimeError(query.lastError().text());
|
||||
|
||||
query.bindValue(DB_COLUMN_NAME.placeholder, QString::fromLatin1("version"));
|
||||
query.bindValue(DB_COLUMN_NAME.placeholder, QString::fromLatin1(META_VERSION));
|
||||
query.bindValue(DB_COLUMN_VALUE.placeholder, DB_VERSION);
|
||||
|
||||
if (!query.exec())
|
||||
@@ -391,6 +449,42 @@ void BitTorrent::DBResumeDataStorage::createDB() const
|
||||
}
|
||||
}
|
||||
|
||||
void BitTorrent::DBResumeDataStorage::updateDBFromVersion1() const
|
||||
{
|
||||
auto db = QSqlDatabase::database(DB_CONNECTION_NAME);
|
||||
|
||||
if (!db.transaction())
|
||||
throw RuntimeError(db.lastError().text());
|
||||
|
||||
QSqlQuery query {db};
|
||||
|
||||
try
|
||||
{
|
||||
const auto alterTableTorrentsQuery = QString::fromLatin1("ALTER TABLE %1 ADD %2")
|
||||
.arg(quoted(DB_TABLE_TORRENTS), makeColumnDefinition(DB_COLUMN_DOWNLOAD_PATH, "TEXT"));
|
||||
if (!query.exec(alterTableTorrentsQuery))
|
||||
throw RuntimeError(query.lastError().text());
|
||||
|
||||
const QString updateMetaVersionQuery = makeUpdateStatement(DB_TABLE_META, {DB_COLUMN_NAME, DB_COLUMN_VALUE});
|
||||
if (!query.prepare(updateMetaVersionQuery))
|
||||
throw RuntimeError(query.lastError().text());
|
||||
|
||||
query.bindValue(DB_COLUMN_NAME.placeholder, QString::fromLatin1(META_VERSION));
|
||||
query.bindValue(DB_COLUMN_VALUE.placeholder, DB_VERSION);
|
||||
|
||||
if (!query.exec())
|
||||
throw RuntimeError(query.lastError().text());
|
||||
|
||||
if (!db.commit())
|
||||
throw RuntimeError(db.lastError().text());
|
||||
}
|
||||
catch (const RuntimeError &)
|
||||
{
|
||||
db.rollback();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
BitTorrent::DBResumeDataStorage::Worker::Worker(const QString &dbPath, const QString &dbConnectionName)
|
||||
: m_path {dbPath}
|
||||
, m_connectionName {dbConnectionName}
|
||||
@@ -499,7 +593,6 @@ void BitTorrent::DBResumeDataStorage::Worker::store(const TorrentID &id, const L
|
||||
query.bindValue(DB_COLUMN_CATEGORY.placeholder, resumeData.category);
|
||||
query.bindValue(DB_COLUMN_TAGS.placeholder, (resumeData.tags.isEmpty()
|
||||
? QVariant(QVariant::String) : resumeData.tags.join(QLatin1String(","))));
|
||||
query.bindValue(DB_COLUMN_TARGET_SAVE_PATH.placeholder, Profile::instance()->toPortablePath(resumeData.savePath));
|
||||
query.bindValue(DB_COLUMN_CONTENT_LAYOUT.placeholder, Utils::String::fromEnum(resumeData.contentLayout));
|
||||
query.bindValue(DB_COLUMN_RATIO_LIMIT.placeholder, static_cast<int>(resumeData.ratioLimit * 1000));
|
||||
query.bindValue(DB_COLUMN_SEEDING_TIME_LIMIT.placeholder, resumeData.seedingTimeLimit);
|
||||
@@ -507,6 +600,13 @@ void BitTorrent::DBResumeDataStorage::Worker::store(const TorrentID &id, const L
|
||||
query.bindValue(DB_COLUMN_HAS_SEED_STATUS.placeholder, resumeData.hasSeedStatus);
|
||||
query.bindValue(DB_COLUMN_OPERATING_MODE.placeholder, Utils::String::fromEnum(resumeData.operatingMode));
|
||||
query.bindValue(DB_COLUMN_STOPPED.placeholder, resumeData.stopped);
|
||||
|
||||
if (!resumeData.useAutoTMM)
|
||||
{
|
||||
query.bindValue(DB_COLUMN_TARGET_SAVE_PATH.placeholder, Profile::instance()->toPortablePath(resumeData.savePath));
|
||||
query.bindValue(DB_COLUMN_DOWNLOAD_PATH.placeholder, Profile::instance()->toPortablePath(resumeData.downloadPath));
|
||||
}
|
||||
|
||||
query.bindValue(DB_COLUMN_RESUMEDATA.placeholder, bencodedResumeData);
|
||||
if (!bencodedMetadata.isEmpty())
|
||||
query.bindValue(DB_COLUMN_METADATA.placeholder, bencodedMetadata);
|
||||
|
||||
@@ -50,7 +50,9 @@ namespace BitTorrent
|
||||
void storeQueue(const QVector<TorrentID> &queue) const override;
|
||||
|
||||
private:
|
||||
int currentDBVersion() const;
|
||||
void createDB() const;
|
||||
void updateDBFromVersion1() const;
|
||||
|
||||
QThread *m_ioThread = nullptr;
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
#include "base/bittorrent/infohash.h"
|
||||
|
||||
void FileSearcher::search(const BitTorrent::TorrentID &id, const QStringList &originalFileNames
|
||||
, const QString &completeSavePath, const QString &incompleteSavePath)
|
||||
, const QString &savePath, const QString &downloadPath)
|
||||
{
|
||||
const auto findInDir = [](const QString &dirPath, QStringList &fileNames) -> bool
|
||||
{
|
||||
@@ -56,14 +56,14 @@ void FileSearcher::search(const BitTorrent::TorrentID &id, const QStringList &or
|
||||
return found;
|
||||
};
|
||||
|
||||
QString savePath = completeSavePath;
|
||||
QString usedPath = savePath;
|
||||
QStringList adjustedFileNames = originalFileNames;
|
||||
const bool found = findInDir(savePath, adjustedFileNames);
|
||||
if (!found && !incompleteSavePath.isEmpty())
|
||||
const bool found = findInDir(usedPath, adjustedFileNames);
|
||||
if (!found && !downloadPath.isEmpty())
|
||||
{
|
||||
savePath = incompleteSavePath;
|
||||
findInDir(savePath, adjustedFileNames);
|
||||
usedPath = downloadPath;
|
||||
findInDir(usedPath, adjustedFileNames);
|
||||
}
|
||||
|
||||
emit searchFinished(id, savePath, adjustedFileNames);
|
||||
emit searchFinished(id, usedPath, adjustedFileNames);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ public:
|
||||
|
||||
public slots:
|
||||
void search(const BitTorrent::TorrentID &id, const QStringList &originalFileNames
|
||||
, const QString &completeSavePath, const QString &incompleteSavePath);
|
||||
, const QString &savePath, const QString &downloadPath);
|
||||
|
||||
signals:
|
||||
void searchFinished(const BitTorrent::TorrentID &id, const QString &savePath, const QStringList &fileNames);
|
||||
|
||||
@@ -46,8 +46,10 @@ namespace BitTorrent
|
||||
QString category;
|
||||
TagSet tags;
|
||||
QString savePath;
|
||||
QString downloadPath;
|
||||
TorrentContentLayout contentLayout = TorrentContentLayout::Original;
|
||||
TorrentOperatingMode operatingMode = TorrentOperatingMode::AutoManaged;
|
||||
bool useAutoTMM = false;
|
||||
bool firstLastPiecePriority = false;
|
||||
bool hasSeedStatus = false;
|
||||
bool stopped = false;
|
||||
|
||||
@@ -58,6 +58,10 @@
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QHostAddress>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QNetworkAddressEntry>
|
||||
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
|
||||
#include <QNetworkConfigurationManager>
|
||||
@@ -80,6 +84,7 @@
|
||||
#include "base/unicodestrings.h"
|
||||
#include "base/utils/bytearray.h"
|
||||
#include "base/utils/fs.h"
|
||||
#include "base/utils/io.h"
|
||||
#include "base/utils/misc.h"
|
||||
#include "base/utils/net.h"
|
||||
#include "base/utils/random.h"
|
||||
@@ -103,6 +108,8 @@
|
||||
|
||||
using namespace BitTorrent;
|
||||
|
||||
const QString CATEGORIES_FILE_NAME {QStringLiteral("categories.json")};
|
||||
|
||||
namespace
|
||||
{
|
||||
const char PEER_ID[] = "qB";
|
||||
@@ -156,42 +163,9 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
QStringMap map_cast(const QVariantMap &map)
|
||||
QMap<QString, CategoryOptions> expandCategories(const QMap<QString, CategoryOptions> &categories)
|
||||
{
|
||||
QStringMap result;
|
||||
for (auto i = map.cbegin(); i != map.cend(); ++i)
|
||||
result[i.key()] = i.value().toString();
|
||||
return result;
|
||||
}
|
||||
|
||||
QVariantMap map_cast(const QStringMap &map)
|
||||
{
|
||||
QVariantMap result;
|
||||
for (auto i = map.cbegin(); i != map.cend(); ++i)
|
||||
result[i.key()] = i.value();
|
||||
return result;
|
||||
}
|
||||
|
||||
QString normalizePath(const QString &path)
|
||||
{
|
||||
QString tmp = Utils::Fs::toUniformPath(path.trimmed());
|
||||
if (!tmp.isEmpty() && !tmp.endsWith('/'))
|
||||
return tmp + '/';
|
||||
return tmp;
|
||||
}
|
||||
|
||||
QString normalizeSavePath(QString path, const QString &defaultPath = specialFolderLocation(SpecialFolder::Downloads))
|
||||
{
|
||||
path = path.trimmed();
|
||||
if (path.isEmpty())
|
||||
path = Utils::Fs::toUniformPath(defaultPath.trimmed());
|
||||
|
||||
return normalizePath(path);
|
||||
}
|
||||
|
||||
QStringMap expandCategories(const QStringMap &categories)
|
||||
{
|
||||
QStringMap expanded = categories;
|
||||
QMap<QString, CategoryOptions> expanded = categories;
|
||||
|
||||
for (auto i = categories.cbegin(); i != categories.cend(); ++i)
|
||||
{
|
||||
@@ -199,7 +173,7 @@ namespace
|
||||
for (const QString &subcat : asConst(Session::expandCategory(category)))
|
||||
{
|
||||
if (!expanded.contains(subcat))
|
||||
expanded[subcat] = "";
|
||||
expanded[subcat] = {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,13 +392,12 @@ Session::Session(QObject *parent)
|
||||
, clampValue(ChokingAlgorithm::FixedSlots, ChokingAlgorithm::RateBased))
|
||||
, m_seedChokingAlgorithm(BITTORRENT_SESSION_KEY("SeedChokingAlgorithm"), SeedChokingAlgorithm::FastestUpload
|
||||
, clampValue(SeedChokingAlgorithm::RoundRobin, SeedChokingAlgorithm::AntiLeech))
|
||||
, m_storedCategories(BITTORRENT_SESSION_KEY("Categories"))
|
||||
, m_storedTags(BITTORRENT_SESSION_KEY("Tags"))
|
||||
, m_maxRatioAction(BITTORRENT_SESSION_KEY("MaxRatioAction"), Pause)
|
||||
, m_defaultSavePath(BITTORRENT_SESSION_KEY("DefaultSavePath"), specialFolderLocation(SpecialFolder::Downloads), normalizePath)
|
||||
, m_tempPath(BITTORRENT_SESSION_KEY("TempPath"), defaultSavePath() + "temp/", normalizePath)
|
||||
, m_savePath(BITTORRENT_SESSION_KEY("DefaultSavePath"), specialFolderLocation(SpecialFolder::Downloads), Utils::Fs::toUniformPath)
|
||||
, m_downloadPath(BITTORRENT_SESSION_KEY("TempPath"), specialFolderLocation(SpecialFolder::Downloads) + QLatin1String("/temp"), Utils::Fs::toUniformPath)
|
||||
, m_isSubcategoriesEnabled(BITTORRENT_SESSION_KEY("SubcategoriesEnabled"), false)
|
||||
, m_isTempPathEnabled(BITTORRENT_SESSION_KEY("TempPathEnabled"), false)
|
||||
, m_isDownloadPathEnabled(BITTORRENT_SESSION_KEY("TempPathEnabled"), false)
|
||||
, m_isAutoTMMDisabledByDefault(BITTORRENT_SESSION_KEY("DisableAutoTMMByDefault"), true)
|
||||
, m_isDisableAutoTMMWhenCategoryChanged(BITTORRENT_SESSION_KEY("DisableAutoTMMTriggers/CategoryChanged"), false)
|
||||
, m_isDisableAutoTMMWhenDefaultSavePathChanged(BITTORRENT_SESSION_KEY("DisableAutoTMMTriggers/DefaultSavePathChanged"), true)
|
||||
@@ -472,12 +445,11 @@ Session::Session(QObject *parent)
|
||||
if (isBandwidthSchedulerEnabled())
|
||||
enableBandwidthScheduler();
|
||||
|
||||
m_categories = map_cast(m_storedCategories);
|
||||
loadCategories();
|
||||
if (isSubcategoriesEnabled())
|
||||
{
|
||||
// if subcategories support changed manually
|
||||
m_categories = expandCategories(m_categories);
|
||||
m_storedCategories = map_cast(m_categories);
|
||||
}
|
||||
|
||||
const QStringList storedTags = m_storedTags.get();
|
||||
@@ -566,18 +538,18 @@ void Session::setPeXEnabled(const bool enabled)
|
||||
LogMsg(tr("Restart is required to toggle PeX support"), Log::WARNING);
|
||||
}
|
||||
|
||||
bool Session::isTempPathEnabled() const
|
||||
bool Session::isDownloadPathEnabled() const
|
||||
{
|
||||
return m_isTempPathEnabled;
|
||||
return m_isDownloadPathEnabled;
|
||||
}
|
||||
|
||||
void Session::setTempPathEnabled(const bool enabled)
|
||||
void Session::setDownloadPathEnabled(const bool enabled)
|
||||
{
|
||||
if (enabled != isTempPathEnabled())
|
||||
if (enabled != isDownloadPathEnabled())
|
||||
{
|
||||
m_isTempPathEnabled = enabled;
|
||||
m_isDownloadPathEnabled = enabled;
|
||||
for (TorrentImpl *const torrent : asConst(m_torrents))
|
||||
torrent->handleTempPathChanged();
|
||||
torrent->handleDownloadPathChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -645,22 +617,14 @@ void Session::setFinishedTorrentExportDirectory(QString path)
|
||||
m_finishedTorrentExportDirectory = path;
|
||||
}
|
||||
|
||||
QString Session::defaultSavePath() const
|
||||
QString Session::savePath() const
|
||||
{
|
||||
return Utils::Fs::toUniformPath(m_defaultSavePath);
|
||||
return m_savePath;
|
||||
}
|
||||
|
||||
QString Session::tempPath() const
|
||||
QString Session::downloadPath() const
|
||||
{
|
||||
return Utils::Fs::toUniformPath(m_tempPath);
|
||||
}
|
||||
|
||||
QString Session::torrentTempPath(const TorrentInfo &torrentInfo) const
|
||||
{
|
||||
if ((torrentInfo.filesCount() > 1) && !torrentInfo.hasRootFolder())
|
||||
return tempPath() + torrentInfo.name() + '/';
|
||||
|
||||
return tempPath();
|
||||
return m_downloadPath;
|
||||
}
|
||||
|
||||
bool Session::isValidCategoryName(const QString &name)
|
||||
@@ -692,29 +656,53 @@ QStringList Session::expandCategory(const QString &category)
|
||||
return result;
|
||||
}
|
||||
|
||||
QStringMap Session::categories() const
|
||||
QStringList Session::categories() const
|
||||
{
|
||||
return m_categories;
|
||||
return m_categories.keys();
|
||||
}
|
||||
|
||||
CategoryOptions Session::categoryOptions(const QString &categoryName) const
|
||||
{
|
||||
return m_categories.value(categoryName);
|
||||
}
|
||||
|
||||
QString Session::categorySavePath(const QString &categoryName) const
|
||||
{
|
||||
const QString basePath = m_defaultSavePath;
|
||||
if (categoryName.isEmpty()) return basePath;
|
||||
const QString basePath = savePath();
|
||||
if (categoryName.isEmpty())
|
||||
return basePath;
|
||||
|
||||
QString path = m_categories.value(categoryName);
|
||||
QString path = m_categories.value(categoryName).savePath;
|
||||
if (path.isEmpty()) // use implicit save path
|
||||
path = Utils::Fs::toValidFileSystemName(categoryName, true);
|
||||
|
||||
if (!QDir::isAbsolutePath(path))
|
||||
path.prepend(basePath);
|
||||
|
||||
return normalizeSavePath(path);
|
||||
return (QDir::isAbsolutePath(path) ? path : Utils::Fs::resolvePath(path, basePath));
|
||||
}
|
||||
|
||||
bool Session::addCategory(const QString &name, const QString &savePath)
|
||||
QString Session::categoryDownloadPath(const QString &categoryName) const
|
||||
{
|
||||
if (name.isEmpty()) return false;
|
||||
const CategoryOptions categoryOptions = m_categories.value(categoryName);
|
||||
const CategoryOptions::DownloadPathOption downloadPathOption =
|
||||
categoryOptions.downloadPath.value_or(CategoryOptions::DownloadPathOption {isDownloadPathEnabled(), downloadPath()});
|
||||
if (!downloadPathOption.enabled)
|
||||
return {};
|
||||
|
||||
const QString basePath = downloadPath();
|
||||
if (categoryName.isEmpty())
|
||||
return basePath;
|
||||
|
||||
const QString path = (!downloadPathOption.path.isEmpty()
|
||||
? downloadPathOption.path
|
||||
: Utils::Fs::toValidFileSystemName(categoryName, true)); // use implicit download path
|
||||
|
||||
return (QDir::isAbsolutePath(path) ? path : Utils::Fs::resolvePath(path, basePath));
|
||||
}
|
||||
|
||||
bool Session::addCategory(const QString &name, const CategoryOptions &options)
|
||||
{
|
||||
if (name.isEmpty())
|
||||
return false;
|
||||
|
||||
if (!isValidCategoryName(name) || m_categories.contains(name))
|
||||
return false;
|
||||
|
||||
@@ -724,37 +712,46 @@ bool Session::addCategory(const QString &name, const QString &savePath)
|
||||
{
|
||||
if ((parent != name) && !m_categories.contains(parent))
|
||||
{
|
||||
m_categories[parent] = "";
|
||||
m_categories[parent] = {};
|
||||
emit categoryAdded(parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_categories[name] = savePath;
|
||||
m_storedCategories = map_cast(m_categories);
|
||||
m_categories[name] = options;
|
||||
storeCategories();
|
||||
emit categoryAdded(name);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Session::editCategory(const QString &name, const QString &savePath)
|
||||
bool Session::editCategory(const QString &name, const CategoryOptions &options)
|
||||
{
|
||||
if (!m_categories.contains(name)) return false;
|
||||
if (categorySavePath(name) == savePath) return false;
|
||||
const auto it = m_categories.find(name);
|
||||
if (it == m_categories.end())
|
||||
return false;
|
||||
|
||||
m_categories[name] = savePath;
|
||||
m_storedCategories = map_cast(m_categories);
|
||||
CategoryOptions ¤tOptions = it.value();
|
||||
if (options == currentOptions)
|
||||
return false;
|
||||
|
||||
currentOptions = options;
|
||||
storeCategories();
|
||||
if (isDisableAutoTMMWhenCategorySavePathChanged())
|
||||
{
|
||||
for (TorrentImpl *const torrent : asConst(m_torrents))
|
||||
{
|
||||
if (torrent->category() == name)
|
||||
torrent->setAutoTMMEnabled(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (TorrentImpl *const torrent : asConst(m_torrents))
|
||||
{
|
||||
if (torrent->category() == name)
|
||||
torrent->handleCategorySavePathChanged();
|
||||
torrent->handleCategoryOptionsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -763,8 +760,10 @@ bool Session::editCategory(const QString &name, const QString &savePath)
|
||||
bool Session::removeCategory(const QString &name)
|
||||
{
|
||||
for (TorrentImpl *const torrent : asConst(m_torrents))
|
||||
{
|
||||
if (torrent->belongsToCategory(name))
|
||||
torrent->setCategory("");
|
||||
}
|
||||
|
||||
// remove stored category and its subcategories if exist
|
||||
bool result = false;
|
||||
@@ -772,7 +771,7 @@ bool Session::removeCategory(const QString &name)
|
||||
{
|
||||
// remove subcategories
|
||||
const QString test = name + '/';
|
||||
Algorithm::removeIf(m_categories, [this, &test, &result](const QString &category, const QString &)
|
||||
Algorithm::removeIf(m_categories, [this, &test, &result](const QString &category, const CategoryOptions &)
|
||||
{
|
||||
if (category.startsWith(test))
|
||||
{
|
||||
@@ -789,7 +788,7 @@ bool Session::removeCategory(const QString &name)
|
||||
if (result)
|
||||
{
|
||||
// update stored categories
|
||||
m_storedCategories = map_cast(m_categories);
|
||||
storeCategories();
|
||||
emit categoryRemoved(name);
|
||||
}
|
||||
|
||||
@@ -810,12 +809,12 @@ void Session::setSubcategoriesEnabled(const bool value)
|
||||
// expand categories to include all parent categories
|
||||
m_categories = expandCategories(m_categories);
|
||||
// update stored categories
|
||||
m_storedCategories = map_cast(m_categories);
|
||||
storeCategories();
|
||||
}
|
||||
else
|
||||
{
|
||||
// reload categories
|
||||
m_categories = map_cast(m_storedCategories);
|
||||
loadCategories();
|
||||
}
|
||||
|
||||
m_isSubcategoriesEnabled = value;
|
||||
@@ -1823,14 +1822,7 @@ bool Session::deleteTorrent(const TorrentID &id, const DeleteOption deleteOption
|
||||
}
|
||||
else
|
||||
{
|
||||
QString rootPath = torrent->rootPath(true);
|
||||
if (!rootPath.isEmpty() && torrent->useTempPath())
|
||||
{
|
||||
// torrent without root folder still has it in its temporary save path
|
||||
rootPath = torrent->actualStorageLocation();
|
||||
}
|
||||
|
||||
m_removingTorrents[torrent->id()] = {torrent->name(), rootPath, deleteOption};
|
||||
m_removingTorrents[torrent->id()] = {torrent->name(), torrent->rootPath(), deleteOption};
|
||||
|
||||
if (m_moveStorageQueue.size() > 1)
|
||||
{
|
||||
@@ -2059,6 +2051,7 @@ LoadTorrentParams Session::initLoadTorrentParams(const AddTorrentParams &addTorr
|
||||
LoadTorrentParams loadTorrentParams;
|
||||
|
||||
loadTorrentParams.name = addTorrentParams.name;
|
||||
loadTorrentParams.useAutoTMM = addTorrentParams.useAutoTMM.value_or(!isAutoTMMDisabledByDefault());
|
||||
loadTorrentParams.firstLastPiecePriority = addTorrentParams.firstLastPiecePriority;
|
||||
loadTorrentParams.hasSeedStatus = addTorrentParams.skipChecking; // do not react on 'torrent_finished_alert' when skipping
|
||||
loadTorrentParams.contentLayout = addTorrentParams.contentLayout.value_or(torrentContentLayout());
|
||||
@@ -2067,21 +2060,26 @@ LoadTorrentParams Session::initLoadTorrentParams(const AddTorrentParams &addTorr
|
||||
loadTorrentParams.ratioLimit = addTorrentParams.ratioLimit;
|
||||
loadTorrentParams.seedingTimeLimit = addTorrentParams.seedingTimeLimit;
|
||||
|
||||
const bool useAutoTMM = addTorrentParams.useAutoTMM.value_or(!isAutoTMMDisabledByDefault());
|
||||
if (useAutoTMM)
|
||||
loadTorrentParams.savePath = "";
|
||||
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);
|
||||
if (!loadTorrentParams.useAutoTMM)
|
||||
{
|
||||
loadTorrentParams.savePath = (QDir::isAbsolutePath(addTorrentParams.savePath)
|
||||
? addTorrentParams.savePath
|
||||
: Utils::Fs::resolvePath(addTorrentParams.savePath, savePath()));
|
||||
|
||||
const bool useDownloadPath = addTorrentParams.useDownloadPath.value_or(isDownloadPathEnabled());
|
||||
if (useDownloadPath)
|
||||
{
|
||||
loadTorrentParams.downloadPath = (QDir::isAbsolutePath(addTorrentParams.downloadPath)
|
||||
? addTorrentParams.downloadPath
|
||||
: Utils::Fs::resolvePath(addTorrentParams.downloadPath, downloadPath()));
|
||||
}
|
||||
}
|
||||
|
||||
const QString category = addTorrentParams.category;
|
||||
if (!category.isEmpty() && !m_categories.contains(category) && !addCategory(category))
|
||||
loadTorrentParams.category = "";
|
||||
else
|
||||
loadTorrentParams.category = addTorrentParams.category;
|
||||
loadTorrentParams.category = category;
|
||||
|
||||
for (const QString &tag : addTorrentParams.tags)
|
||||
{
|
||||
@@ -2143,8 +2141,9 @@ bool Session::addTorrent_impl(const std::variant<MagnetUri, TorrentInfo> &source
|
||||
|
||||
bool isFindingIncompleteFiles = false;
|
||||
|
||||
// If empty then Automatic mode, otherwise Manual mode
|
||||
const QString actualSavePath = loadTorrentParams.savePath.isEmpty() ? categorySavePath(loadTorrentParams.category) : loadTorrentParams.savePath;
|
||||
const bool useAutoTMM = loadTorrentParams.useAutoTMM;
|
||||
const QString actualSavePath = useAutoTMM ? categorySavePath(loadTorrentParams.category) : loadTorrentParams.savePath;
|
||||
|
||||
if (hasMetadata)
|
||||
{
|
||||
const TorrentInfo &torrentInfo = std::get<TorrentInfo>(source);
|
||||
@@ -2170,7 +2169,9 @@ bool Session::addTorrent_impl(const std::variant<MagnetUri, TorrentInfo> &source
|
||||
|
||||
if (!loadTorrentParams.hasSeedStatus)
|
||||
{
|
||||
findIncompleteFiles(torrentInfo, actualSavePath, filePaths);
|
||||
const QString actualDownloadPath = useAutoTMM
|
||||
? categoryDownloadPath(loadTorrentParams.category) : loadTorrentParams.downloadPath;
|
||||
findIncompleteFiles(torrentInfo, actualSavePath, actualDownloadPath, filePaths);
|
||||
isFindingIncompleteFiles = true;
|
||||
}
|
||||
|
||||
@@ -2264,17 +2265,16 @@ bool Session::loadTorrent(LoadTorrentParams params)
|
||||
return true;
|
||||
}
|
||||
|
||||
void Session::findIncompleteFiles(const TorrentInfo &torrentInfo, const QString &savePath, const QStringList &filePaths) const
|
||||
void Session::findIncompleteFiles(const TorrentInfo &torrentInfo, const QString &savePath
|
||||
, const QString &downloadPath, const QStringList &filePaths) const
|
||||
{
|
||||
Q_ASSERT(filePaths.isEmpty() || (filePaths.size() == torrentInfo.filesCount()));
|
||||
|
||||
const auto searchId = TorrentID::fromInfoHash(torrentInfo.infoHash());
|
||||
const QStringList originalFileNames = (filePaths.isEmpty() ? torrentInfo.filePaths() : filePaths);
|
||||
const QString completeSavePath = savePath;
|
||||
const QString incompleteSavePath = (isTempPathEnabled() ? torrentTempPath(torrentInfo) : QString {});
|
||||
QMetaObject::invokeMethod(m_fileSearcher, [=]()
|
||||
{
|
||||
m_fileSearcher->search(searchId, originalFileNames, completeSavePath, incompleteSavePath);
|
||||
m_fileSearcher->search(searchId, originalFileNames, savePath, downloadPath);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2432,30 +2432,37 @@ void Session::removeTorrentsQueue() const
|
||||
m_resumeDataStorage->storeQueue({});
|
||||
}
|
||||
|
||||
void Session::setDefaultSavePath(QString path)
|
||||
void Session::setSavePath(const QString &path)
|
||||
{
|
||||
path = normalizeSavePath(path);
|
||||
if (path == m_defaultSavePath) return;
|
||||
const QString baseSavePath = specialFolderLocation(SpecialFolder::Downloads);
|
||||
const QString resolvedPath = (QDir::isAbsolutePath(path) ? path : Utils::Fs::resolvePath(path, baseSavePath));
|
||||
if (resolvedPath == m_savePath) return;
|
||||
|
||||
m_defaultSavePath = path;
|
||||
m_savePath = resolvedPath;
|
||||
|
||||
if (isDisableAutoTMMWhenDefaultSavePathChanged())
|
||||
{
|
||||
for (TorrentImpl *const torrent : asConst(m_torrents))
|
||||
torrent->setAutoTMMEnabled(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (TorrentImpl *const torrent : asConst(m_torrents))
|
||||
torrent->handleCategorySavePathChanged();
|
||||
torrent->handleCategoryOptionsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void Session::setTempPath(QString path)
|
||||
void Session::setDownloadPath(const QString &path)
|
||||
{
|
||||
path = normalizeSavePath(path, defaultSavePath() + "temp/");
|
||||
if (path == m_tempPath) return;
|
||||
const QString baseDownloadPath = specialFolderLocation(SpecialFolder::Downloads) + QLatin1String("/temp");
|
||||
const QString resolvedPath = (QDir::isAbsolutePath(path) ? path : Utils::Fs::resolvePath(path, baseDownloadPath));
|
||||
if (resolvedPath != m_downloadPath)
|
||||
{
|
||||
m_downloadPath = resolvedPath;
|
||||
|
||||
m_tempPath = path;
|
||||
|
||||
for (TorrentImpl *const torrent : asConst(m_torrents))
|
||||
torrent->handleTempPathChanged();
|
||||
for (TorrentImpl *const torrent : asConst(m_torrents))
|
||||
torrent->handleDownloadPathChanged();
|
||||
}
|
||||
}
|
||||
|
||||
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
|
||||
@@ -3969,7 +3976,7 @@ void Session::handleTorrentFinished(TorrentImpl *const torrent)
|
||||
if (torrentRelpath.endsWith(".torrent", Qt::CaseInsensitive))
|
||||
{
|
||||
qDebug("Found possible recursive torrent download.");
|
||||
const QString torrentFullpath = torrent->savePath(true) + '/' + torrentRelpath;
|
||||
const QString torrentFullpath = torrent->actualStorageLocation() + '/' + torrentRelpath;
|
||||
qDebug("Full subtorrent path is %s", qUtf8Printable(torrentFullpath));
|
||||
if (TorrentInfo::loadFromFile(torrentFullpath))
|
||||
{
|
||||
@@ -4022,7 +4029,7 @@ bool Session::addMoveTorrentStorageJob(TorrentImpl *torrent, const QString &newP
|
||||
Q_ASSERT(torrent);
|
||||
|
||||
const lt::torrent_handle torrentHandle = torrent->nativeHandle();
|
||||
const QString currentLocation = torrent->actualStorageLocation();
|
||||
const QString currentLocation = Utils::Fs::toNativePath(torrent->actualStorageLocation());
|
||||
|
||||
if (m_moveStorageQueue.size() > 1)
|
||||
{
|
||||
@@ -4116,6 +4123,89 @@ void Session::handleMoveTorrentStorageJobFinished()
|
||||
}
|
||||
}
|
||||
|
||||
void Session::storeCategories() const
|
||||
{
|
||||
QJsonObject jsonObj;
|
||||
for (auto it = m_categories.cbegin(); it != m_categories.cend(); ++it)
|
||||
{
|
||||
const QString &categoryName = it.key();
|
||||
const CategoryOptions &categoryOptions = it.value();
|
||||
jsonObj[categoryName] = categoryOptions.toJSON();
|
||||
}
|
||||
|
||||
const QString path = QDir(specialFolderLocation(SpecialFolder::Config)).absoluteFilePath(CATEGORIES_FILE_NAME);
|
||||
const QByteArray data = QJsonDocument(jsonObj).toJson();
|
||||
const nonstd::expected<void, QString> result = Utils::IO::saveToFile(path, data);
|
||||
if (!result)
|
||||
{
|
||||
LogMsg(tr("Couldn't store Categories configuration to %1. Error: %2")
|
||||
.arg(path, result.error()), Log::WARNING);
|
||||
}
|
||||
}
|
||||
|
||||
void Session::upgradeCategories()
|
||||
{
|
||||
const auto legacyCategories = SettingValue<QVariantMap>("BitTorrent/Session/Categories").get();
|
||||
for (auto it = legacyCategories.cbegin(); it != legacyCategories.cend(); ++it)
|
||||
{
|
||||
const QString categoryName = it.key();
|
||||
CategoryOptions categoryOptions;
|
||||
categoryOptions.savePath = it.value().toString();
|
||||
m_categories[categoryName] = categoryOptions;
|
||||
}
|
||||
|
||||
storeCategories();
|
||||
}
|
||||
|
||||
void Session::loadCategories()
|
||||
{
|
||||
m_categories.clear();
|
||||
|
||||
QFile confFile {QDir(specialFolderLocation(SpecialFolder::Config)).absoluteFilePath(CATEGORIES_FILE_NAME)};
|
||||
if (!confFile.exists())
|
||||
{
|
||||
// TODO: Remove the following upgrade code in v4.5
|
||||
// == BEGIN UPGRADE CODE ==
|
||||
upgradeCategories();
|
||||
m_needUpgradeDownloadPath = true;
|
||||
// == END UPGRADE CODE ==
|
||||
|
||||
// return;
|
||||
}
|
||||
|
||||
if (!confFile.open(QFile::ReadOnly))
|
||||
{
|
||||
LogMsg(tr("Couldn't load Categories from %1. Error: %2")
|
||||
.arg(confFile.fileName(), confFile.errorString()), Log::CRITICAL);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonParseError jsonError;
|
||||
const QJsonDocument jsonDoc = QJsonDocument::fromJson(confFile.readAll(), &jsonError);
|
||||
if (jsonError.error != QJsonParseError::NoError)
|
||||
{
|
||||
LogMsg(tr("Couldn't parse Categories configuration from %1. Error: %2")
|
||||
.arg(confFile.fileName(), jsonError.errorString()), Log::WARNING);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!jsonDoc.isObject())
|
||||
{
|
||||
LogMsg(tr("Couldn't load Categories 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 &categoryName = it.key();
|
||||
const auto categoryOptions = CategoryOptions::fromJSON(it.value().toObject());
|
||||
m_categories[categoryName] = categoryOptions;
|
||||
}
|
||||
}
|
||||
|
||||
void Session::handleTorrentTrackerWarning(TorrentImpl *const torrent, const QString &trackerUrl)
|
||||
{
|
||||
emit trackerWarning(torrent, trackerUrl);
|
||||
@@ -4258,20 +4348,40 @@ void Session::startUpTorrents()
|
||||
QVector<TorrentID> queue;
|
||||
for (const TorrentID &torrentID : torrents)
|
||||
{
|
||||
const std::optional<LoadTorrentParams> resumeData = startupStorage->load(torrentID);
|
||||
if (resumeData)
|
||||
const std::optional<LoadTorrentParams> loadResumeDataResult = startupStorage->load(torrentID);
|
||||
if (loadResumeDataResult)
|
||||
{
|
||||
LoadTorrentParams resumeData = *loadResumeDataResult;
|
||||
bool needStore = false;
|
||||
|
||||
if (m_resumeDataStorage != startupStorage)
|
||||
{
|
||||
m_resumeDataStorage->store(torrentID, *resumeData);
|
||||
if (isQueueingSystemEnabled() && !resumeData->hasSeedStatus)
|
||||
needStore = true;
|
||||
if (isQueueingSystemEnabled() && !resumeData.hasSeedStatus)
|
||||
queue.append(torrentID);
|
||||
}
|
||||
|
||||
// TODO: Remove the following upgrade code in v4.5
|
||||
// == BEGIN UPGRADE CODE ==
|
||||
if (m_needUpgradeDownloadPath && isDownloadPathEnabled())
|
||||
{
|
||||
if (!resumeData.useAutoTMM)
|
||||
{
|
||||
resumeData.downloadPath = downloadPath();
|
||||
needStore = true;
|
||||
}
|
||||
}
|
||||
// == END UPGRADE CODE ==
|
||||
|
||||
if (needStore)
|
||||
m_resumeDataStorage->store(torrentID, resumeData);
|
||||
|
||||
qDebug() << "Starting up torrent" << torrentID.toString() << "...";
|
||||
if (!loadTorrent(*resumeData))
|
||||
if (!loadTorrent(resumeData))
|
||||
{
|
||||
LogMsg(tr("Unable to resume torrent '%1'.", "e.g: Unable to resume torrent 'hash'.")
|
||||
.arg(torrentID.toString()), Log::CRITICAL);
|
||||
.arg(torrentID.toString()), Log::CRITICAL);
|
||||
}
|
||||
|
||||
// process add torrent messages before message queue overflow
|
||||
if ((resumedTorrentsCount % 100) == 0) readAlerts();
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
#include "base/types.h"
|
||||
#include "addtorrentparams.h"
|
||||
#include "cachestatus.h"
|
||||
#include "categoryoptions.h"
|
||||
#include "sessionstatus.h"
|
||||
#include "torrentinfo.h"
|
||||
#include "trackerentry.h"
|
||||
@@ -211,22 +212,23 @@ namespace BitTorrent
|
||||
static void freeInstance();
|
||||
static Session *instance();
|
||||
|
||||
QString defaultSavePath() const;
|
||||
void setDefaultSavePath(QString path);
|
||||
QString tempPath() const;
|
||||
void setTempPath(QString path);
|
||||
bool isTempPathEnabled() const;
|
||||
void setTempPathEnabled(bool enabled);
|
||||
QString torrentTempPath(const TorrentInfo &torrentInfo) const;
|
||||
QString savePath() const;
|
||||
void setSavePath(const QString &path);
|
||||
QString downloadPath() const;
|
||||
void setDownloadPath(const QString &path);
|
||||
bool isDownloadPathEnabled() const;
|
||||
void setDownloadPathEnabled(bool enabled);
|
||||
|
||||
static bool isValidCategoryName(const QString &name);
|
||||
// returns category itself and all top level categories
|
||||
static QStringList expandCategory(const QString &category);
|
||||
|
||||
QStringMap categories() const;
|
||||
QStringList categories() const;
|
||||
CategoryOptions categoryOptions(const QString &categoryName) const;
|
||||
QString categorySavePath(const QString &categoryName) const;
|
||||
bool addCategory(const QString &name, const QString &savePath = "");
|
||||
bool editCategory(const QString &name, const QString &savePath);
|
||||
QString categoryDownloadPath(const QString &categoryName) const;
|
||||
bool addCategory(const QString &name, const CategoryOptions &options = {});
|
||||
bool editCategory(const QString &name, const CategoryOptions &options);
|
||||
bool removeCategory(const QString &name);
|
||||
bool isSubcategoriesEnabled() const;
|
||||
void setSubcategoriesEnabled(bool value);
|
||||
@@ -499,7 +501,8 @@ namespace BitTorrent
|
||||
|
||||
bool addMoveTorrentStorageJob(TorrentImpl *torrent, const QString &newPath, MoveStorageMode mode);
|
||||
|
||||
void findIncompleteFiles(const TorrentInfo &torrentInfo, const QString &savePath, const QStringList &filePaths = {}) const;
|
||||
void findIncompleteFiles(const TorrentInfo &torrentInfo, const QString &savePath
|
||||
, const QString &downloadPath, const QStringList &filePaths = {}) const;
|
||||
|
||||
signals:
|
||||
void allTorrentsFinished();
|
||||
@@ -642,6 +645,10 @@ namespace BitTorrent
|
||||
void moveTorrentStorage(const MoveStorageJob &job) const;
|
||||
void handleMoveTorrentStorageJobFinished();
|
||||
|
||||
void loadCategories();
|
||||
void storeCategories() const;
|
||||
void upgradeCategories();
|
||||
|
||||
// BitTorrent
|
||||
lt::session *m_nativeSession = nullptr;
|
||||
|
||||
@@ -729,13 +736,12 @@ namespace BitTorrent
|
||||
CachedSettingValue<bool> m_isProxyPeerConnectionsEnabled;
|
||||
CachedSettingValue<ChokingAlgorithm> m_chokingAlgorithm;
|
||||
CachedSettingValue<SeedChokingAlgorithm> m_seedChokingAlgorithm;
|
||||
CachedSettingValue<QVariantMap> m_storedCategories;
|
||||
CachedSettingValue<QStringList> m_storedTags;
|
||||
CachedSettingValue<int> m_maxRatioAction;
|
||||
CachedSettingValue<QString> m_defaultSavePath;
|
||||
CachedSettingValue<QString> m_tempPath;
|
||||
CachedSettingValue<QString> m_savePath;
|
||||
CachedSettingValue<QString> m_downloadPath;
|
||||
CachedSettingValue<bool> m_isSubcategoriesEnabled;
|
||||
CachedSettingValue<bool> m_isTempPathEnabled;
|
||||
CachedSettingValue<bool> m_isDownloadPathEnabled;
|
||||
CachedSettingValue<bool> m_isAutoTMMDisabledByDefault;
|
||||
CachedSettingValue<bool> m_isDisableAutoTMMWhenCategoryChanged;
|
||||
CachedSettingValue<bool> m_isDisableAutoTMMWhenDefaultSavePathChanged;
|
||||
@@ -780,7 +786,7 @@ namespace BitTorrent
|
||||
QHash<QString, AddTorrentParams> m_downloadedTorrents;
|
||||
QHash<TorrentID, RemovingTorrentData> m_removingTorrents;
|
||||
QSet<TorrentID> m_needSaveResumeDataTorrents;
|
||||
QStringMap m_categories;
|
||||
QMap<QString, CategoryOptions> m_categories;
|
||||
QSet<QString> m_tags;
|
||||
|
||||
// I/O errored torrents
|
||||
@@ -800,6 +806,8 @@ namespace BitTorrent
|
||||
|
||||
QString m_lastExternalIP;
|
||||
|
||||
bool m_needUpgradeDownloadPath = false;
|
||||
|
||||
static Session *m_instance;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -126,11 +126,10 @@ namespace BitTorrent
|
||||
virtual QString currentTracker() const = 0;
|
||||
|
||||
// 1. savePath() - the path where all the files and subfolders of torrent are stored.
|
||||
// 1.1 downloadPath() - the path where all the files and subfolders of torrent are stored until torrent has finished downloading.
|
||||
// 2. rootPath() - absolute path of torrent file tree (first common subfolder of torrent files); empty string if torrent has no root folder.
|
||||
// 3. contentPath() - absolute path of torrent content (root path for multifile torrents, absolute file path for singlefile torrents).
|
||||
//
|
||||
// These methods have 'actual' parameter (defaults to false) which allow to get actual or final path variant.
|
||||
//
|
||||
// Examples.
|
||||
// Suppose we have three torrent with following structures and save path `/home/user/torrents`:
|
||||
//
|
||||
@@ -168,14 +167,15 @@ namespace BitTorrent
|
||||
// | B | /home/user/torrents/torrentB | /home/user/torrents/torrentB/subdir1/file1 |
|
||||
// | C | <empty> | /home/user/torrents/file1 |
|
||||
|
||||
virtual QString savePath(bool actual = false) const = 0;
|
||||
virtual QString rootPath(bool actual = false) const = 0;
|
||||
virtual QString contentPath(bool actual = false) const = 0;
|
||||
|
||||
virtual bool useTempPath() const = 0;
|
||||
|
||||
virtual bool isAutoTMMEnabled() const = 0;
|
||||
virtual void setAutoTMMEnabled(bool enabled) = 0;
|
||||
virtual QString savePath() const = 0;
|
||||
virtual void setSavePath(const QString &savePath) = 0;
|
||||
virtual QString downloadPath() const = 0;
|
||||
virtual void setDownloadPath(const QString &downloadPath) = 0;
|
||||
virtual QString actualStorageLocation() const = 0;
|
||||
virtual QString rootPath() const = 0;
|
||||
virtual QString contentPath() const = 0;
|
||||
virtual QString category() const = 0;
|
||||
virtual bool belongsToCategory(const QString &category) const = 0;
|
||||
virtual bool setCategory(const QString &category) = 0;
|
||||
@@ -273,7 +273,6 @@ namespace BitTorrent
|
||||
virtual void setFirstLastPiecePriority(bool enabled) = 0;
|
||||
virtual void pause() = 0;
|
||||
virtual void resume(TorrentOperatingMode mode = TorrentOperatingMode::AutoManaged) = 0;
|
||||
virtual void move(QString path) = 0;
|
||||
virtual void forceReannounce(int index = -1) = 0;
|
||||
virtual void forceDHTAnnounce() = 0;
|
||||
virtual void forceRecheck() = 0;
|
||||
|
||||
@@ -252,7 +252,8 @@ TorrentImpl::TorrentImpl(Session *session, lt::session *nativeSession
|
||||
, m_infoHash(m_nativeHandle.info_hash())
|
||||
#endif
|
||||
, m_name(params.name)
|
||||
, m_savePath(Utils::Fs::toNativePath(params.savePath))
|
||||
, m_savePath(params.savePath)
|
||||
, m_downloadPath(params.downloadPath)
|
||||
, m_category(params.category)
|
||||
, m_tags(params.tags)
|
||||
, m_ratioLimit(params.ratioLimit)
|
||||
@@ -261,13 +262,10 @@ TorrentImpl::TorrentImpl(Session *session, lt::session *nativeSession
|
||||
, m_contentLayout(params.contentLayout)
|
||||
, m_hasSeedStatus(params.hasSeedStatus)
|
||||
, m_hasFirstLastPiecePriority(params.firstLastPiecePriority)
|
||||
, m_useAutoTMM(params.savePath.isEmpty())
|
||||
, m_useAutoTMM(params.useAutoTMM)
|
||||
, m_isStopped(params.stopped)
|
||||
, m_ltAddTorrentParams(params.ltAddTorrentParams)
|
||||
{
|
||||
if (m_useAutoTMM)
|
||||
m_savePath = Utils::Fs::toNativePath(m_session->categorySavePath(m_category));
|
||||
|
||||
if (m_ltAddTorrentParams.ti)
|
||||
{
|
||||
// Initialize it only if torrent is added with metadata.
|
||||
@@ -293,7 +291,7 @@ TorrentImpl::TorrentImpl(Session *session, lt::session *nativeSession
|
||||
if (hasMetadata())
|
||||
applyFirstLastPiecePriority(m_hasFirstLastPiecePriority);
|
||||
|
||||
// TODO: Remove the following upgrade code in v.4.4
|
||||
// TODO: Remove the following upgrade code in v4.4
|
||||
// == BEGIN UPGRADE CODE ==
|
||||
const QString spath = actualStorageLocation();
|
||||
for (int i = 0; i < filesCount(); ++i)
|
||||
@@ -396,15 +394,52 @@ QString TorrentImpl::currentTracker() const
|
||||
return QString::fromStdString(m_nativeStatus.current_tracker);
|
||||
}
|
||||
|
||||
QString TorrentImpl::savePath(bool actual) const
|
||||
QString TorrentImpl::savePath() const
|
||||
{
|
||||
if (actual)
|
||||
return Utils::Fs::toUniformPath(actualStorageLocation());
|
||||
else
|
||||
return Utils::Fs::toUniformPath(m_savePath);
|
||||
return isAutoTMMEnabled() ? m_session->categorySavePath(category()) : m_savePath;
|
||||
}
|
||||
|
||||
QString TorrentImpl::rootPath(bool actual) const
|
||||
void TorrentImpl::setSavePath(const QString &path)
|
||||
{
|
||||
Q_ASSERT(!isAutoTMMEnabled());
|
||||
|
||||
const QString resolvedPath = (QDir::isAbsolutePath(path) ? path : Utils::Fs::resolvePath(path, m_session->savePath()));
|
||||
if (resolvedPath == savePath())
|
||||
return;
|
||||
|
||||
m_savePath = resolvedPath;
|
||||
|
||||
m_session->handleTorrentNeedSaveResumeData(this);
|
||||
|
||||
const bool isFinished = isSeed() || m_hasSeedStatus;
|
||||
if (isFinished)
|
||||
moveStorage(savePath(), MoveStorageMode::KeepExistingFiles);
|
||||
}
|
||||
|
||||
QString TorrentImpl::downloadPath() const
|
||||
{
|
||||
return isAutoTMMEnabled() ? m_session->categoryDownloadPath(category()) : m_downloadPath;
|
||||
}
|
||||
|
||||
void TorrentImpl::setDownloadPath(const QString &path)
|
||||
{
|
||||
Q_ASSERT(!isAutoTMMEnabled());
|
||||
|
||||
const QString resolvedPath = ((path.isEmpty() || QDir::isAbsolutePath(path))
|
||||
? path : Utils::Fs::resolvePath(path, m_session->downloadPath()));
|
||||
if (resolvedPath == m_downloadPath)
|
||||
return;
|
||||
|
||||
m_downloadPath = resolvedPath;
|
||||
|
||||
m_session->handleTorrentNeedSaveResumeData(this);
|
||||
|
||||
const bool isFinished = isSeed() || m_hasSeedStatus;
|
||||
if (!isFinished)
|
||||
moveStorage((m_downloadPath.isEmpty() ? savePath() : m_downloadPath), MoveStorageMode::KeepExistingFiles);
|
||||
}
|
||||
|
||||
QString TorrentImpl::rootPath() const
|
||||
{
|
||||
if (!hasMetadata())
|
||||
return {};
|
||||
@@ -413,19 +448,19 @@ QString TorrentImpl::rootPath(bool actual) const
|
||||
if (relativeRootPath.isEmpty())
|
||||
return {};
|
||||
|
||||
return QDir(savePath(actual)).absoluteFilePath(relativeRootPath);
|
||||
return QDir(actualStorageLocation()).absoluteFilePath(relativeRootPath);
|
||||
}
|
||||
|
||||
QString TorrentImpl::contentPath(const bool actual) const
|
||||
QString TorrentImpl::contentPath() const
|
||||
{
|
||||
if (!hasMetadata())
|
||||
return {};
|
||||
|
||||
if (filesCount() == 1)
|
||||
return QDir(savePath(actual)).absoluteFilePath(filePath(0));
|
||||
return QDir(actualStorageLocation()).absoluteFilePath(filePath(0));
|
||||
|
||||
const QString rootPath = this->rootPath(actual);
|
||||
return (rootPath.isEmpty() ? savePath(actual) : rootPath);
|
||||
const QString rootPath = this->rootPath();
|
||||
return (rootPath.isEmpty() ? actualStorageLocation() : rootPath);
|
||||
}
|
||||
|
||||
bool TorrentImpl::isAutoTMMEnabled() const
|
||||
@@ -435,19 +470,25 @@ bool TorrentImpl::isAutoTMMEnabled() const
|
||||
|
||||
void TorrentImpl::setAutoTMMEnabled(bool enabled)
|
||||
{
|
||||
if (m_useAutoTMM == enabled) return;
|
||||
if (m_useAutoTMM == enabled)
|
||||
return;
|
||||
|
||||
m_useAutoTMM = enabled;
|
||||
if (!m_useAutoTMM)
|
||||
{
|
||||
m_savePath = m_session->categorySavePath(category());
|
||||
m_downloadPath = m_session->categoryDownloadPath(category());
|
||||
}
|
||||
|
||||
m_session->handleTorrentNeedSaveResumeData(this);
|
||||
m_session->handleTorrentSavingModeChanged(this);
|
||||
|
||||
if (m_useAutoTMM)
|
||||
move_impl(m_session->categorySavePath(m_category), MoveStorageMode::Overwrite);
|
||||
adjustStorageLocation();
|
||||
}
|
||||
|
||||
QString TorrentImpl::actualStorageLocation() const
|
||||
{
|
||||
return QString::fromStdString(m_nativeStatus.save_path);
|
||||
return Utils::Fs::toUniformPath(QString::fromStdString(m_nativeStatus.save_path));
|
||||
}
|
||||
|
||||
void TorrentImpl::setAutoManaged(const bool enable)
|
||||
@@ -774,7 +815,7 @@ QStringList TorrentImpl::absoluteFilePaths() const
|
||||
{
|
||||
if (!hasMetadata()) return {};
|
||||
|
||||
const QDir saveDir {savePath(true)};
|
||||
const QDir saveDir {actualStorageLocation()};
|
||||
QStringList res;
|
||||
res.reserve(filesCount());
|
||||
for (int i = 0; i < filesCount(); ++i)
|
||||
@@ -1351,7 +1392,7 @@ bool TorrentImpl::setCategory(const QString &category)
|
||||
if (m_useAutoTMM)
|
||||
{
|
||||
if (!m_session->isDisableAutoTMMWhenCategoryChanged())
|
||||
move_impl(m_session->categorySavePath(m_category), MoveStorageMode::Overwrite);
|
||||
adjustStorageLocation();
|
||||
else
|
||||
setAutoTMMEnabled(false);
|
||||
}
|
||||
@@ -1360,41 +1401,6 @@ bool TorrentImpl::setCategory(const QString &category)
|
||||
return true;
|
||||
}
|
||||
|
||||
void TorrentImpl::move(QString path)
|
||||
{
|
||||
if (m_useAutoTMM)
|
||||
{
|
||||
m_useAutoTMM = false;
|
||||
m_session->handleTorrentNeedSaveResumeData(this);
|
||||
m_session->handleTorrentSavingModeChanged(this);
|
||||
}
|
||||
|
||||
path = Utils::Fs::toUniformPath(path.trimmed());
|
||||
if (path.isEmpty())
|
||||
path = m_session->defaultSavePath();
|
||||
if (!path.endsWith('/'))
|
||||
path += '/';
|
||||
|
||||
move_impl(path, MoveStorageMode::KeepExistingFiles);
|
||||
}
|
||||
|
||||
void TorrentImpl::move_impl(QString path, const MoveStorageMode mode)
|
||||
{
|
||||
if (path == savePath()) return;
|
||||
|
||||
path = Utils::Fs::toNativePath(path);
|
||||
if (!useTempPath())
|
||||
{
|
||||
moveStorage(path, mode);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_savePath = path;
|
||||
m_session->handleTorrentNeedSaveResumeData(this);
|
||||
m_session->handleTorrentSavePathChanged(this);
|
||||
}
|
||||
}
|
||||
|
||||
void TorrentImpl::forceReannounce(const int index)
|
||||
{
|
||||
m_nativeHandle.force_reannounce(0, index);
|
||||
@@ -1611,7 +1617,7 @@ void TorrentImpl::resume(const TorrentOperatingMode mode)
|
||||
|
||||
void TorrentImpl::moveStorage(const QString &newPath, const MoveStorageMode mode)
|
||||
{
|
||||
if (m_session->addMoveTorrentStorageJob(this, newPath, mode))
|
||||
if (m_session->addMoveTorrentStorageJob(this, Utils::Fs::toNativePath(newPath), mode))
|
||||
{
|
||||
m_storageIsMoving = true;
|
||||
updateStatus();
|
||||
@@ -1629,18 +1635,18 @@ void TorrentImpl::handleStateUpdate(const lt::torrent_status &nativeStatus)
|
||||
updateStatus(nativeStatus);
|
||||
}
|
||||
|
||||
void TorrentImpl::handleDownloadPathChanged()
|
||||
{
|
||||
adjustStorageLocation();
|
||||
}
|
||||
|
||||
void TorrentImpl::handleMoveStorageJobFinished(const bool hasOutstandingJob)
|
||||
{
|
||||
m_session->handleTorrentNeedSaveResumeData(this);
|
||||
m_storageIsMoving = hasOutstandingJob;
|
||||
|
||||
updateStatus();
|
||||
const QString newPath = QString::fromStdString(m_nativeStatus.save_path);
|
||||
if (!useTempPath() && (newPath != m_savePath))
|
||||
{
|
||||
m_savePath = newPath;
|
||||
m_session->handleTorrentSavePathChanged(this);
|
||||
}
|
||||
m_session->handleTorrentSavePathChanged(this);
|
||||
|
||||
if (!m_storageIsMoving)
|
||||
{
|
||||
@@ -1717,7 +1723,7 @@ void TorrentImpl::handleTorrentCheckedAlert(const lt::torrent_checked_alert *p)
|
||||
else if (progress() == 1.0)
|
||||
m_hasSeedStatus = true;
|
||||
|
||||
adjustActualSavePath();
|
||||
adjustStorageLocation();
|
||||
manageIncompleteFiles();
|
||||
}
|
||||
|
||||
@@ -1735,7 +1741,7 @@ void TorrentImpl::handleTorrentFinishedAlert(const lt::torrent_finished_alert *p
|
||||
updateStatus();
|
||||
m_hasSeedStatus = true;
|
||||
|
||||
adjustActualSavePath();
|
||||
adjustStorageLocation();
|
||||
manageIncompleteFiles();
|
||||
|
||||
m_session->handleTorrentNeedSaveResumeData(this);
|
||||
@@ -1778,7 +1784,7 @@ void TorrentImpl::handleSaveResumeDataAlert(const lt::save_resume_data_alert *p)
|
||||
|
||||
QStringList filePaths = metadata.filePaths();
|
||||
applyContentLayout(filePaths, m_contentLayout);
|
||||
m_session->findIncompleteFiles(metadata, m_savePath, filePaths);
|
||||
m_session->findIncompleteFiles(metadata, savePath(), downloadPath(), filePaths);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1812,7 +1818,6 @@ void TorrentImpl::prepareResumeData(const lt::add_torrent_params ¶ms)
|
||||
LoadTorrentParams resumeData;
|
||||
resumeData.name = m_name;
|
||||
resumeData.category = m_category;
|
||||
resumeData.savePath = m_useAutoTMM ? "" : m_savePath;
|
||||
resumeData.tags = m_tags;
|
||||
resumeData.contentLayout = m_contentLayout;
|
||||
resumeData.ratioLimit = m_ratioLimit;
|
||||
@@ -1822,6 +1827,12 @@ void TorrentImpl::prepareResumeData(const lt::add_torrent_params ¶ms)
|
||||
resumeData.stopped = m_isStopped;
|
||||
resumeData.operatingMode = m_operatingMode;
|
||||
resumeData.ltAddTorrentParams = m_ltAddTorrentParams;
|
||||
resumeData.useAutoTMM = m_useAutoTMM;
|
||||
if (!resumeData.useAutoTMM)
|
||||
{
|
||||
resumeData.savePath = m_savePath;
|
||||
resumeData.downloadPath = m_downloadPath;
|
||||
}
|
||||
|
||||
m_session->handleTorrentResumeDataReady(this, resumeData);
|
||||
}
|
||||
@@ -1956,15 +1967,10 @@ void TorrentImpl::handlePerformanceAlert(const lt::performance_alert *p) const
|
||||
, Log::INFO);
|
||||
}
|
||||
|
||||
void TorrentImpl::handleTempPathChanged()
|
||||
{
|
||||
adjustActualSavePath();
|
||||
}
|
||||
|
||||
void TorrentImpl::handleCategorySavePathChanged()
|
||||
void TorrentImpl::handleCategoryOptionsChanged()
|
||||
{
|
||||
if (m_useAutoTMM)
|
||||
move_impl(m_session->categorySavePath(m_category), MoveStorageMode::Overwrite);
|
||||
adjustStorageLocation();
|
||||
}
|
||||
|
||||
void TorrentImpl::handleAppendExtensionToggled()
|
||||
@@ -2069,39 +2075,13 @@ void TorrentImpl::manageIncompleteFiles()
|
||||
}
|
||||
}
|
||||
|
||||
void TorrentImpl::adjustActualSavePath()
|
||||
void TorrentImpl::adjustStorageLocation()
|
||||
{
|
||||
if (!isMoveInProgress())
|
||||
adjustActualSavePath_impl();
|
||||
else
|
||||
m_moveFinishedTriggers.append([this]() { adjustActualSavePath_impl(); });
|
||||
}
|
||||
const QString downloadPath = this->downloadPath();
|
||||
const bool isFinished = isSeed() || m_hasSeedStatus;
|
||||
const QDir targetDir {((isFinished || downloadPath.isEmpty()) ? savePath() : downloadPath)};
|
||||
|
||||
void TorrentImpl::adjustActualSavePath_impl()
|
||||
{
|
||||
const bool needUseTempDir = useTempPath();
|
||||
const QDir tempDir {m_session->torrentTempPath(m_torrentInfo)};
|
||||
const QDir currentDir {actualStorageLocation()};
|
||||
const QDir targetDir {needUseTempDir ? tempDir : QDir {savePath()}};
|
||||
|
||||
if (targetDir == currentDir) return;
|
||||
|
||||
if (!needUseTempDir)
|
||||
{
|
||||
if ((currentDir == tempDir) && (currentDir != QDir {m_session->tempPath()}))
|
||||
{
|
||||
// torrent without root folder still has it in its temporary save path
|
||||
// so its temp path isn't equal to temp path root
|
||||
const QString currentDirPath = currentDir.absolutePath();
|
||||
m_moveFinishedTriggers.append([currentDirPath]
|
||||
{
|
||||
qDebug() << "Removing torrent temp folder:" << currentDirPath;
|
||||
Utils::Fs::smartRemoveEmptyFolderTree(currentDirPath);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
moveStorage(Utils::Fs::toNativePath(targetDir.absolutePath()), MoveStorageMode::Overwrite);
|
||||
moveStorage(targetDir.absolutePath(), MoveStorageMode::Overwrite);
|
||||
}
|
||||
|
||||
lt::torrent_handle TorrentImpl::nativeHandle() const
|
||||
@@ -2114,11 +2094,6 @@ bool TorrentImpl::isMoveInProgress() const
|
||||
return m_storageIsMoving;
|
||||
}
|
||||
|
||||
bool TorrentImpl::useTempPath() const
|
||||
{
|
||||
return m_session->isTempPathEnabled() && !(isSeed() || m_hasSeedStatus);
|
||||
}
|
||||
|
||||
void TorrentImpl::updateStatus()
|
||||
{
|
||||
updateStatus(m_nativeHandle.status());
|
||||
|
||||
@@ -99,14 +99,15 @@ namespace BitTorrent
|
||||
qlonglong wastedSize() const override;
|
||||
QString currentTracker() const override;
|
||||
|
||||
QString savePath(bool actual = false) const override;
|
||||
QString rootPath(bool actual = false) const override;
|
||||
QString contentPath(bool actual = false) const override;
|
||||
|
||||
bool useTempPath() const override;
|
||||
|
||||
bool isAutoTMMEnabled() const override;
|
||||
void setAutoTMMEnabled(bool enabled) override;
|
||||
QString savePath() const override;
|
||||
void setSavePath(const QString &path) override;
|
||||
QString downloadPath() const override;
|
||||
void setDownloadPath(const QString &path) override;
|
||||
QString actualStorageLocation() const override;
|
||||
QString rootPath() const override;
|
||||
QString contentPath() const override;
|
||||
QString category() const override;
|
||||
bool belongsToCategory(const QString &category) const override;
|
||||
bool setCategory(const QString &category) override;
|
||||
@@ -201,7 +202,6 @@ namespace BitTorrent
|
||||
void setFirstLastPiecePriority(bool enabled) override;
|
||||
void pause() override;
|
||||
void resume(TorrentOperatingMode mode = TorrentOperatingMode::AutoManaged) override;
|
||||
void move(QString path) override;
|
||||
void forceReannounce(int index = -1) override;
|
||||
void forceDHTAnnounce() override;
|
||||
void forceRecheck() override;
|
||||
@@ -232,15 +232,13 @@ namespace BitTorrent
|
||||
|
||||
void handleAlert(const lt::alert *a);
|
||||
void handleStateUpdate(const lt::torrent_status &nativeStatus);
|
||||
void handleTempPathChanged();
|
||||
void handleCategorySavePathChanged();
|
||||
void handleDownloadPathChanged();
|
||||
void handleCategoryOptionsChanged();
|
||||
void handleAppendExtensionToggled();
|
||||
void saveResumeData();
|
||||
void handleMoveStorageJobFinished(bool hasOutstandingJob);
|
||||
void fileSearchFinished(const QString &savePath, const QStringList &fileNames);
|
||||
|
||||
QString actualStorageLocation() const;
|
||||
|
||||
private:
|
||||
using EventTrigger = std::function<void ()>;
|
||||
|
||||
@@ -272,9 +270,7 @@ namespace BitTorrent
|
||||
|
||||
void setAutoManaged(bool enable);
|
||||
|
||||
void adjustActualSavePath();
|
||||
void adjustActualSavePath_impl();
|
||||
void move_impl(QString path, MoveStorageMode mode);
|
||||
void adjustStorageLocation();
|
||||
void moveStorage(const QString &newPath, MoveStorageMode mode);
|
||||
void manageIncompleteFiles();
|
||||
void applyFirstLastPiecePriority(bool enabled, const QVector<DownloadPriority> &updatedFilePrio = {});
|
||||
@@ -308,6 +304,7 @@ namespace BitTorrent
|
||||
// Persistent data
|
||||
QString m_name;
|
||||
QString m_savePath;
|
||||
QString m_downloadPath;
|
||||
QString m_category;
|
||||
TagSet m_tags;
|
||||
qreal m_ratioLimit;
|
||||
|
||||
@@ -88,8 +88,6 @@ QString Profile::location(const SpecialFolder folder) const
|
||||
break;
|
||||
}
|
||||
|
||||
if (!result.endsWith(QLatin1Char('/')))
|
||||
result += QLatin1Char('/');
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,8 @@ 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_USEDOWNLOADPATH {QStringLiteral("use_download_path")};
|
||||
const QString PARAM_DOWNLOADPATH {QStringLiteral("download_path")};
|
||||
const QString PARAM_OPERATINGMODE {QStringLiteral("operating_mode")};
|
||||
const QString PARAM_STOPPED {QStringLiteral("stopped")};
|
||||
const QString PARAM_SKIPCHECKING {QStringLiteral("skip_checking")};
|
||||
@@ -136,6 +138,8 @@ namespace
|
||||
params.category = jsonObj.value(PARAM_CATEGORY).toString();
|
||||
params.tags = parseTagSet(jsonObj.value(PARAM_TAGS).toArray());
|
||||
params.savePath = jsonObj.value(PARAM_SAVEPATH).toString();
|
||||
params.useDownloadPath = getOptionalBool(jsonObj, PARAM_USEDOWNLOADPATH);
|
||||
params.downloadPath = jsonObj.value(PARAM_DOWNLOADPATH).toString();
|
||||
params.addForced = (getEnum<BitTorrent::TorrentOperatingMode>(jsonObj, PARAM_OPERATINGMODE) == BitTorrent::TorrentOperatingMode::Forced);
|
||||
params.addPaused = getOptionalBool(jsonObj, PARAM_STOPPED);
|
||||
params.skipChecking = jsonObj.value(PARAM_SKIPCHECKING).toBool();
|
||||
@@ -155,6 +159,7 @@ namespace
|
||||
{PARAM_CATEGORY, params.category},
|
||||
{PARAM_TAGS, serializeTagSet(params.tags)},
|
||||
{PARAM_SAVEPATH, params.savePath},
|
||||
{PARAM_DOWNLOADPATH, params.downloadPath},
|
||||
{PARAM_OPERATINGMODE, Utils::String::fromEnum(params.addForced
|
||||
? BitTorrent::TorrentOperatingMode::Forced : BitTorrent::TorrentOperatingMode::AutoManaged)},
|
||||
{PARAM_SKIPCHECKING, params.skipChecking},
|
||||
@@ -170,6 +175,8 @@ namespace
|
||||
jsonObj[PARAM_CONTENTLAYOUT] = Utils::String::fromEnum(*params.contentLayout);
|
||||
if (params.useAutoTMM)
|
||||
jsonObj[PARAM_AUTOTMM] = *params.useAutoTMM;
|
||||
if (params.useDownloadPath)
|
||||
jsonObj[PARAM_USEDOWNLOADPATH] = *params.useDownloadPath;
|
||||
|
||||
return jsonObj;
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ bool TorrentFilter::setCategory(const QString &category)
|
||||
if ((m_category != category)
|
||||
|| (m_category.isNull() && !category.isNull())
|
||||
|| (!m_category.isNull() && category.isNull()))
|
||||
{
|
||||
{
|
||||
m_category = category;
|
||||
return true;
|
||||
}
|
||||
@@ -141,7 +141,7 @@ bool TorrentFilter::setTag(const QString &tag)
|
||||
if ((m_tag != tag)
|
||||
|| (m_tag.isNull() && !tag.isNull())
|
||||
|| (!m_tag.isNull() && tag.isNull()))
|
||||
{
|
||||
{
|
||||
m_tag = tag;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -72,9 +72,14 @@ QString Utils::Fs::toUniformPath(const QString &path)
|
||||
return QDir::fromNativeSeparators(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file extension part of a file name.
|
||||
*/
|
||||
QString Utils::Fs::resolvePath(const QString &relativePath, const QString &basePath)
|
||||
{
|
||||
Q_ASSERT(QDir::isRelativePath(relativePath));
|
||||
Q_ASSERT(QDir::isAbsolutePath(basePath));
|
||||
|
||||
return (relativePath.isEmpty() ? basePath : QDir(basePath).absoluteFilePath(relativePath));
|
||||
}
|
||||
|
||||
QString Utils::Fs::fileExtension(const QString &filename)
|
||||
{
|
||||
const QString name = filename.endsWith(QB_EXT)
|
||||
|
||||
@@ -42,6 +42,7 @@ namespace Utils::Fs
|
||||
* with the OS being run.
|
||||
*/
|
||||
QString toNativePath(const QString &path);
|
||||
|
||||
/**
|
||||
* Converts a path to a string suitable for processing.
|
||||
* This function makes sure the directory separator used is independent
|
||||
@@ -50,7 +51,16 @@ namespace Utils::Fs
|
||||
*/
|
||||
QString toUniformPath(const QString &path);
|
||||
|
||||
/**
|
||||
* If `path is relative then resolves it against `basePath`, otherwise returns the `path` itself
|
||||
*/
|
||||
QString resolvePath(const QString &relativePath, const QString &basePath);
|
||||
|
||||
/**
|
||||
* Returns the file extension part of a file name.
|
||||
*/
|
||||
QString fileExtension(const QString &filename);
|
||||
|
||||
QString fileName(const QString &filePath);
|
||||
QString folderName(const QString &filePath);
|
||||
qint64 computePathSize(const QString &path);
|
||||
|
||||
Reference in New Issue
Block a user