Skip to content

Commit ceb7aa1

Browse files
committed
Simplify UniqueSubfolder layout and migration
Fold hash naming into TorrentContentLayout::UniqueSubfolder, always keep the full 12-hex tag (truncate the name from the end to fit platform filename limits), and reduce right-click migrate to rename-if-free only.
1 parent 56a90df commit ceb7aa1

28 files changed

Lines changed: 273 additions & 356 deletions

src/base/bittorrent/addtorrentparams.cpp

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,6 @@ BitTorrent::AddTorrentParams BitTorrent::parseAddTorrentParams(const QJsonObject
126126
.filePaths = {},
127127
.filePriorities = {},
128128
.seedMode = seedModeValue.toBool(jsonObj.value(DEPRECATED_PARAM_SKIP_CHECKING).toBool()),
129-
.preserveOriginalPayloadName = jsonObj.value(u"preserveOriginalPayloadName"_s).toBool(),
130129
.contentLayout = getOptionalEnum<TorrentContentLayout>(jsonObj, PARAM_CONTENTLAYOUT),
131130
.useAutoTMM = getOptionalBool(jsonObj, PARAM_AUTOTMM),
132131
.uploadLimit = jsonObj.value(PARAM_UPLOADLIMIT).toInt(-1),
@@ -160,7 +159,6 @@ QJsonObject BitTorrent::serializeAddTorrentParams(const AddTorrentParams &params
160159
{PARAM_OPERATINGMODE, Utils::String::fromEnum(params.addForced
161160
? TorrentOperatingMode::Forced : TorrentOperatingMode::AutoManaged)},
162161
{PARAM_SEEDMODE, params.seedMode},
163-
{u"preserveOriginalPayloadName"_s, params.preserveOriginalPayloadName},
164162
{PARAM_UPLOADLIMIT, params.uploadLimit},
165163
{PARAM_DOWNLOADLIMIT, params.downloadLimit},
166164
{PARAM_RATIOLIMIT, params.shareLimits.ratioLimit},

src/base/bittorrent/addtorrentparams.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,6 @@ namespace BitTorrent
6464
PathList filePaths; // used if TorrentInfo is set
6565
QList<DownloadPriority> filePriorities; // used if TorrentInfo is set
6666
bool seedMode = false;
67-
// When Session::isAppendHashToPayloadNamesEnabled(), skip deterministic hash suffix for this torrent.
68-
bool preserveOriginalPayloadName = false;
6967
std::optional<BitTorrent::TorrentContentLayout> contentLayout;
7068
std::optional<bool> useAutoTMM;
7169
int uploadLimit = -1;

src/base/bittorrent/bencoderesumedatastorage.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,6 @@ BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::loadTorre
245245
torrentParams.comment = fromLTString(resumeDataRoot.dict_find_string_value("qBt-comment"));
246246
torrentParams.hasFinishedStatus = resumeDataRoot.dict_find_int_value("qBt-seedStatus");
247247
torrentParams.firstLastPiecePriority = resumeDataRoot.dict_find_int_value("qBt-firstLastPiecePriority");
248-
torrentParams.appendHashToPayloadName = resumeDataRoot.dict_find_int_value("qBt-appendHashToPayloadName");
249248

250249
const lt::string_view ratioLimitString = resumeDataRoot.dict_find_string_value("qBt-ratioLimit");
251250
torrentParams.shareLimits = {
@@ -286,6 +285,10 @@ BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::loadTorre
286285
// fromLTString(root.dict_find_string_value("qBt-contentLayout")), TorrentContentLayout::Default);
287286
// === END REPLACEMENT CODE === //
288287

288+
// Legacy flag from intermediate builds; content_layout is source of truth now.
289+
if (resumeDataRoot.dict_find_int_value("qBt-appendHashToPayloadName"))
290+
torrentParams.contentLayout = TorrentContentLayout::UniqueSubfolder;
291+
289292
torrentParams.stopCondition = Utils::String::toEnum(
290293
fromLTString(resumeDataRoot.dict_find_string_value("qBt-stopCondition")), Torrent::StopCondition::None);
291294
torrentParams.sslParameters =
@@ -466,7 +469,6 @@ void BitTorrent::BencodeResumeDataStorage::Worker::store(const TorrentID &id, co
466469
data["qBt-seedStatus"] = resumeData.hasFinishedStatus;
467470
data["qBt-contentLayout"] = Utils::String::fromEnum(resumeData.contentLayout).toStdString();
468471
data["qBt-firstLastPiecePriority"] = resumeData.firstLastPiecePriority;
469-
data["qBt-appendHashToPayloadName"] = resumeData.appendHashToPayloadName;
470472
data["qBt-stopCondition"] = Utils::String::fromEnum(resumeData.stopCondition).toStdString();
471473

472474
if (!resumeData.sslParameters.certificate.isNull())

src/base/bittorrent/dbresumedatastorage.cpp

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -648,7 +648,6 @@ LoadResumeDataResult DBResumeDataStorage::parseQueryResultRow(const QSqlQuery &q
648648
}
649649
resumeData.hasFinishedStatus = query.value(DB_COLUMN_HAS_SEED_STATUS.name).toBool();
650650
resumeData.firstLastPiecePriority = query.value(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY.name).toBool();
651-
resumeData.appendHashToPayloadName = query.value(DB_COLUMN_APPEND_HASH_TO_PAYLOAD_NAME.name).toBool();
652651
resumeData.shareLimits = {
653652
.ratioLimit = query.value(DB_COLUMN_RATIO_LIMIT.name).toInt() / 1000.0,
654653
.seedingTimeLimit = query.value(DB_COLUMN_SEEDING_TIME_LIMIT.name).toInt(),
@@ -657,7 +656,10 @@ LoadResumeDataResult DBResumeDataStorage::parseQueryResultRow(const QSqlQuery &q
657656
.action = Utils::String::toEnum(query.value(DB_COLUMN_SHARE_LIMIT_ACTION.name).toString(), ShareLimitAction::Default)
658657
};
659658
resumeData.contentLayout = Utils::String::toEnum<TorrentContentLayout>(
660-
query.value(DB_COLUMN_CONTENT_LAYOUT.name).toString(), TorrentContentLayout::Original);
659+
query.value(DB_COLUMN_CONTENT_LAYOUT.name).toString(), TorrentContentLayout::Original);
660+
// Legacy column (pre-UniqueSubfolder layout enum); content_layout is source of truth now.
661+
if (query.value(DB_COLUMN_APPEND_HASH_TO_PAYLOAD_NAME.name).toBool())
662+
resumeData.contentLayout = TorrentContentLayout::UniqueSubfolder;
661663
resumeData.operatingMode = Utils::String::toEnum<TorrentOperatingMode>(
662664
query.value(DB_COLUMN_OPERATING_MODE.name).toString(), TorrentOperatingMode::AutoManaged);
663665
resumeData.stopped = query.value(DB_COLUMN_STOPPED.name).toBool();
@@ -945,7 +947,8 @@ StoreJob::StoreJob(const TorrentID &torrentID, LoadTorrentParams resumeData)
945947
query.bindValue(DB_COLUMN_SHARE_LIMIT_ACTION.placeholder, Utils::String::fromEnum(m_resumeData.shareLimits.action));
946948
query.bindValue(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY.placeholder, m_resumeData.firstLastPiecePriority);
947949
query.bindValue(DB_COLUMN_HAS_SEED_STATUS.placeholder, m_resumeData.hasFinishedStatus);
948-
query.bindValue(DB_COLUMN_APPEND_HASH_TO_PAYLOAD_NAME.placeholder, m_resumeData.appendHashToPayloadName);
950+
// Column kept for schema compatibility; always 0. Layout is in content_layout.
951+
query.bindValue(DB_COLUMN_APPEND_HASH_TO_PAYLOAD_NAME.placeholder, false);
949952
query.bindValue(DB_COLUMN_OPERATING_MODE.placeholder, Utils::String::fromEnum(m_resumeData.operatingMode));
950953
query.bindValue(DB_COLUMN_STOPPED.placeholder, m_resumeData.stopped);
951954
query.bindValue(DB_COLUMN_STOP_CONDITION.placeholder, Utils::String::fromEnum(m_resumeData.stopCondition));

src/base/bittorrent/loadtorrentparams.h

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,6 @@ namespace BitTorrent
5656
bool useAutoTMM = false;
5757
bool firstLastPiecePriority = false;
5858
bool hasFinishedStatus = false;
59-
// Set at add time from Session option + per-torrent preserve override.
60-
// Not re-evaluated later so existing torrents (and pending magnets) are not renamed when the option changes.
61-
bool appendHashToPayloadName = false;
6259
bool stopped = false;
6360
Torrent::StopCondition stopCondition = Torrent::StopCondition::None;
6461

src/base/bittorrent/session.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,8 +220,6 @@ namespace BitTorrent
220220
virtual void setTorrentStopCondition(Torrent::StopCondition stopCondition) = 0;
221221
virtual TorrentContentLayout torrentContentLayout() const = 0;
222222
virtual void setTorrentContentLayout(TorrentContentLayout value) = 0;
223-
virtual bool isAppendHashToPayloadNamesEnabled() const = 0;
224-
virtual void setAppendHashToPayloadNamesEnabled(bool enabled) = 0;
225223
virtual bool isTrackerEnabled() const = 0;
226224
virtual void setTrackerEnabled(bool enabled) = 0;
227225
virtual bool isAppendExtensionEnabled() const = 0;

src/base/bittorrent/sessionimpl.cpp

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@
9898
#include "base/version.h"
9999
#include "bandwidthscheduler.h"
100100
#include "bencoderesumedatastorage.h"
101-
#include "common.h"
102101
#include "customstorage.h"
103102
#include "toplevelpayload.h"
104103
#include "dbresumedatastorage.h"
@@ -567,7 +566,6 @@ SessionImpl::SessionImpl(QObject *parent)
567566
, m_isAddTorrentStopped(BITTORRENT_SESSION_KEY(u"AddTorrentStopped"_s), false)
568567
, m_torrentStopCondition(BITTORRENT_SESSION_KEY(u"TorrentStopCondition"_s), Torrent::StopCondition::None)
569568
, m_torrentContentLayout(BITTORRENT_SESSION_KEY(u"TorrentContentLayout"_s), TorrentContentLayout::Original)
570-
, m_appendHashToPayloadNames(BITTORRENT_SESSION_KEY(u"AppendHashToPayloadNames"_s), false)
571569
, m_isAppendExtensionEnabled(BITTORRENT_SESSION_KEY(u"AddExtensionToIncompleteFiles"_s), false)
572570
, m_isUnwantedFolderEnabled(BITTORRENT_SESSION_KEY(u"UseUnwantedFolder"_s), false)
573571
, m_refreshInterval(BITTORRENT_SESSION_KEY(u"RefreshInterval"_s), 1500)
@@ -2793,8 +2791,6 @@ LoadTorrentParams SessionImpl::initLoadTorrentParams(const AddTorrentParams &add
27932791
loadTorrentParams.name = addTorrentParams.name;
27942792
loadTorrentParams.firstLastPiecePriority = addTorrentParams.firstLastPiecePriority;
27952793
loadTorrentParams.hasFinishedStatus = addTorrentParams.seedMode; // do not react on 'torrent_finished_alert' when skipping
2796-
loadTorrentParams.appendHashToPayloadName = isAppendHashToPayloadNamesEnabled()
2797-
&& !addTorrentParams.preserveOriginalPayloadName;
27982794
loadTorrentParams.contentLayout = addTorrentParams.contentLayout.value_or(torrentContentLayout());
27992795
loadTorrentParams.operatingMode = (addTorrentParams.addForced ? TorrentOperatingMode::Forced : TorrentOperatingMode::AutoManaged);
28002796
loadTorrentParams.stopped = addTorrentParams.addStopped.value_or(isAddTorrentStopped());
@@ -2937,7 +2933,11 @@ bool SessionImpl::addTorrent_impl(const TorrentDescriptor &torrentDescr, const A
29372933
if (filePaths.isEmpty())
29382934
{
29392935
filePaths = torrentInfo.filePaths();
2940-
if (loadTorrentParams.contentLayout != TorrentContentLayout::Original)
2936+
if (loadTorrentParams.contentLayout == TorrentContentLayout::UniqueSubfolder)
2937+
{
2938+
filePaths = applyUniqueSubfolderLayout(std::move(filePaths), id, torrentInfo.name());
2939+
}
2940+
else if (loadTorrentParams.contentLayout != TorrentContentLayout::Original)
29412941
{
29422942
const Path originalRootFolder = Path::findRootFolder(filePaths);
29432943
const auto originalContentLayout = (originalRootFolder.isEmpty()
@@ -2964,11 +2964,6 @@ bool SessionImpl::addTorrent_impl(const TorrentDescriptor &torrentDescr, const A
29642964
loadTorrentParams.name = contentName;
29652965
}
29662966

2967-
// Optional deterministic payload hash naming (decision fixed at add time).
2968-
// Always use the torrent info name so magnet dn= / display name cannot diverge.
2969-
if (loadTorrentParams.appendHashToPayloadName)
2970-
filePaths = applyPayloadHashNaming(std::move(filePaths), id, torrentInfo.name());
2971-
29722967
const auto nativeIndexes = torrentInfo.nativeIndexes();
29732968

29742969
Q_ASSERT(addTorrentParams.filePriorities.isEmpty() || (addTorrentParams.filePriorities.size() == nativeIndexes.size()));
@@ -5995,16 +5990,6 @@ void SessionImpl::setTorrentContentLayout(const TorrentContentLayout value)
59955990
m_torrentContentLayout = value;
59965991
}
59975992

5998-
bool SessionImpl::isAppendHashToPayloadNamesEnabled() const
5999-
{
6000-
return m_appendHashToPayloadNames;
6001-
}
6002-
6003-
void SessionImpl::setAppendHashToPayloadNamesEnabled(const bool enabled)
6004-
{
6005-
m_appendHashToPayloadNames = enabled;
6006-
}
6007-
60085993
// Read alerts sent by libtorrent session
60095994
void SessionImpl::readAlerts()
60105995
{

src/base/bittorrent/sessionimpl.h

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,6 @@ namespace BitTorrent
205205
void setTorrentStopCondition(Torrent::StopCondition stopCondition) override;
206206
TorrentContentLayout torrentContentLayout() const override;
207207
void setTorrentContentLayout(TorrentContentLayout value) override;
208-
bool isAppendHashToPayloadNamesEnabled() const override;
209-
void setAppendHashToPayloadNamesEnabled(bool enabled) override;
210208
bool isTrackerEnabled() const override;
211209
void setTrackerEnabled(bool enabled) override;
212210
bool isAppendExtensionEnabled() const override;
@@ -745,7 +743,6 @@ namespace BitTorrent
745743
CachedSettingValue<bool> m_isAddTorrentStopped;
746744
CachedSettingValue<Torrent::StopCondition> m_torrentStopCondition;
747745
CachedSettingValue<TorrentContentLayout> m_torrentContentLayout;
748-
CachedSettingValue<bool> m_appendHashToPayloadNames;
749746
CachedSettingValue<bool> m_isAppendExtensionEnabled;
750747
CachedSettingValue<bool> m_isUnwantedFolderEnabled;
751748
CachedSettingValue<int> m_refreshInterval;

src/base/bittorrent/toplevelpayload.cpp

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
namespace
3737
{
3838
constexpr int HASH_TAG_HEX_LEN = 12;
39-
constexpr int MAX_COMPONENT_LEN = 255;
4039

4140
PathList renameRootFolder(PathList filePaths, const QString &oldName, const QString &newName)
4241
{
@@ -52,24 +51,45 @@ namespace
5251
return filePaths;
5352
}
5453

55-
// Sanitize and append tag, leaving room for the tag within MAX_COMPONENT_LEN.
56-
QString hashedComponent(const QString &name, const QString &tag)
54+
// Same limits as Utils::Fs::isValidFileName (Win: 255 chars, else 255 UTF-8 bytes).
55+
bool exceedsFileNameLengthLimit(const QString &name)
56+
{
57+
#ifdef Q_OS_WIN
58+
return (name.length() > 255);
59+
#else
60+
return (name.toUtf8().length() > 255);
61+
#endif
62+
}
63+
64+
// Always keep full tag at the end; shorten the name until base+tag fits the platform limit.
65+
QString taggedComponent(const QString &name, const QString &tag)
5766
{
5867
QString base = Utils::Fs::toValidFileName(name.trimmed());
5968
if (base.isEmpty())
6069
base = u"Torrent"_s;
6170

6271
if (base.endsWith(tag))
63-
return base;
72+
base.chop(tag.size());
6473

65-
const int maxBaseLen = std::max(1, MAX_COMPONENT_LEN - static_cast<int>(tag.size()));
66-
if (base.size() > maxBaseLen)
67-
base = base.left(maxBaseLen);
74+
QString result = base + tag;
75+
while (exceedsFileNameLengthLimit(result) && !base.isEmpty())
76+
{
77+
base.chop(1);
78+
#ifdef Q_OS_WIN
79+
// Windows forbids trailing dots/spaces; tag starts with a space so keep base clean.
80+
while (base.endsWith(u'.') || base.endsWith(u' '))
81+
base.chop(1);
82+
#endif
83+
result = base + tag;
84+
}
85+
86+
if (base.isEmpty())
87+
result = u"Torrent"_s + tag;
6888

69-
return base + tag;
89+
return result;
7090
}
7191

72-
QString originalNameForHashDir(const PathList &filePaths, const QString &torrentName, const QString &tag)
92+
QString originalNameForUniqueDir(const PathList &filePaths, const QString &torrentName, const QString &tag)
7393
{
7494
QString base = torrentName.trimmed();
7595
if (base.isEmpty())
@@ -83,43 +103,41 @@ namespace
83103
if (base.isEmpty())
84104
base = u"Torrent"_s;
85105

86-
// Avoid "Name [qb-… ] [qb-…]" if the display/root name already carries this tag.
106+
// Avoid doubling the tag if the name already ends with it.
87107
if (base.endsWith(tag))
88108
base.chop(tag.size());
89109

90110
return base;
91111
}
92112
}
93113

94-
QString BitTorrent::payloadHashTag(const TorrentID &id)
114+
QString BitTorrent::uniqueSubfolderTag(const TorrentID &id)
95115
{
96-
return u" [qb-"_s + id.toString().left(HASH_TAG_HEX_LEN) + u']';
116+
return u' ' + id.toString().left(HASH_TAG_HEX_LEN);
97117
}
98118

99-
QString BitTorrent::payloadHashDirectoryName(const TorrentID &id, const QString &originalName)
119+
QString BitTorrent::uniqueSubfolderName(const TorrentID &id, const QString &originalName)
100120
{
101-
return hashedComponent(originalName, payloadHashTag(id));
121+
return taggedComponent(originalName, uniqueSubfolderTag(id));
102122
}
103123

104-
PathList BitTorrent::applyPayloadHashNaming(PathList filePaths, const TorrentID &id, const QString &torrentName)
124+
PathList BitTorrent::applyUniqueSubfolderLayout(PathList filePaths, const TorrentID &id, const QString &torrentName)
105125
{
106126
if (filePaths.isEmpty())
107127
return filePaths;
108128

109-
const QString tag = payloadHashTag(id);
110-
const QString hashDir = payloadHashDirectoryName(id, originalNameForHashDir(filePaths, torrentName, tag));
129+
const QString tag = uniqueSubfolderTag(id);
130+
const QString folderName = uniqueSubfolderName(id, originalNameForUniqueDir(filePaths, torrentName, tag));
111131

112132
const Path rootFolder = Path::findRootFolder(filePaths);
113133
if (!rootFolder.isEmpty())
114134
{
115-
// Already under the correct hash directory.
116-
if (rootFolder.toString() == hashDir)
135+
if (rootFolder.toString() == folderName)
117136
return filePaths;
118-
// Rename existing top-level folder (e.g. Show/ → Show [qb-HASH]/).
119-
return renameRootFolder(std::move(filePaths), rootFolder.toString(), hashDir);
137+
return renameRootFolder(std::move(filePaths), rootFolder.toString(), folderName);
120138
}
121139

122-
// Single file or rootless multi-file: wrap under the hash directory.
123-
Path::addRootFolder(filePaths, Path(hashDir));
140+
// Single file or rootless multi-file: wrap under the unique folder.
141+
Path::addRootFolder(filePaths, Path(folderName));
124142
return filePaths;
125143
}

src/base/bittorrent/toplevelpayload.h

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -36,43 +36,26 @@
3636

3737
namespace BitTorrent
3838
{
39-
// Stable short tag from TorrentID, e.g. " [qb-a19f83c275d1]".
40-
QString payloadHashTag(const TorrentID &id);
39+
QString uniqueSubfolderTag(const TorrentID &id);
40+
QString uniqueSubfolderName(const TorrentID &id, const QString &originalName);
41+
PathList applyUniqueSubfolderLayout(PathList filePaths, const TorrentID &id, const QString &torrentName);
4142

42-
// Top-level hash directory name: "Original Name [qb-HASH]".
43-
QString payloadHashDirectoryName(const TorrentID &id, const QString &originalName);
44-
45-
// Put every payload path under a single top-level hash directory.
46-
// Idempotent when already under that directory.
47-
PathList applyPayloadHashNaming(PathList filePaths, const TorrentID &id, const QString &torrentName);
48-
49-
// One file rename for the manual conversion.
50-
struct PayloadHashMigrationItem
43+
struct UniqueSubfolderRename
5144
{
5245
int fileIndex = -1;
5346
Path to;
5447
};
5548

56-
// Preflight only — no disk changes.
57-
// If the target hash directory already exists: wipe it entirely after confirmation, then move payload in.
58-
struct PayloadHashMigrationPlan
49+
// Preflight only. If blocked, show blockReason and do nothing.
50+
struct UniqueSubfolderMigrationPlan
5951
{
60-
QList<PayloadHashMigrationItem> renames;
61-
62-
// Absolute path of the existing target hash directory to delete (empty if none).
63-
Path destinationToWipe;
64-
52+
QList<UniqueSubfolderRename> renames;
6553
bool blocked = false;
6654
QString blockReason;
6755

6856
bool isEmpty() const
6957
{
7058
return renames.isEmpty() && !blocked;
7159
}
72-
73-
bool needsConfirmation() const
74-
{
75-
return !destinationToWipe.isEmpty();
76-
}
7760
};
7861
}

0 commit comments

Comments
 (0)