mirror of
https://github.com/qbittorrent/qBittorrent
synced 2025-08-20 21:33:27 -07:00
feat: Add tilde expansion for home directory paths
Enables using ~ and ~/path in configurations instead of hard-coded absolute paths, making settings portable across users. - Add Utils::Fs::expandTilde() function - Integrate tilde expansion in session path handling - Support category path tilde expansion - Add comprehensive test coverage
This commit is contained in:
parent
7a1a214f73
commit
242d33ab53
5 changed files with 108 additions and 8 deletions
|
@ -552,8 +552,8 @@ SessionImpl::SessionImpl(QObject *parent)
|
||||||
, m_storedTags(BITTORRENT_SESSION_KEY(u"Tags"_s))
|
, m_storedTags(BITTORRENT_SESSION_KEY(u"Tags"_s))
|
||||||
, m_shareLimitAction(BITTORRENT_SESSION_KEY(u"ShareLimitAction"_s), ShareLimitAction::Stop
|
, m_shareLimitAction(BITTORRENT_SESSION_KEY(u"ShareLimitAction"_s), ShareLimitAction::Stop
|
||||||
, [](const ShareLimitAction action) { return (action == ShareLimitAction::Default) ? ShareLimitAction::Stop : action; })
|
, [](const ShareLimitAction action) { return (action == ShareLimitAction::Default) ? ShareLimitAction::Stop : action; })
|
||||||
, m_savePath(BITTORRENT_SESSION_KEY(u"DefaultSavePath"_s), specialFolderLocation(SpecialFolder::Downloads))
|
, m_savePath(BITTORRENT_SESSION_KEY(u"DefaultSavePath"_s), specialFolderLocation(SpecialFolder::Downloads), Utils::Fs::expandTilde)
|
||||||
, m_downloadPath(BITTORRENT_SESSION_KEY(u"TempPath"_s), (savePath() / Path(u"temp"_s)))
|
, m_downloadPath(BITTORRENT_SESSION_KEY(u"TempPath"_s), (savePath() / Path(u"temp"_s)), Utils::Fs::expandTilde)
|
||||||
, m_isDownloadPathEnabled(BITTORRENT_SESSION_KEY(u"TempPathEnabled"_s), false)
|
, m_isDownloadPathEnabled(BITTORRENT_SESSION_KEY(u"TempPathEnabled"_s), false)
|
||||||
, m_isSubcategoriesEnabled(BITTORRENT_SESSION_KEY(u"SubcategoriesEnabled"_s), false)
|
, m_isSubcategoriesEnabled(BITTORRENT_SESSION_KEY(u"SubcategoriesEnabled"_s), false)
|
||||||
, m_useCategoryPathsInManualMode(BITTORRENT_SESSION_KEY(u"UseCategoryPathsInManualMode"_s), false)
|
, m_useCategoryPathsInManualMode(BITTORRENT_SESSION_KEY(u"UseCategoryPathsInManualMode"_s), false)
|
||||||
|
@ -1006,6 +1006,14 @@ bool SessionImpl::addCategory(const QString &name, const CategoryOptions &option
|
||||||
if (!isValidCategoryName(name) || m_categories.contains(name))
|
if (!isValidCategoryName(name) || m_categories.contains(name))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
// Expand tildes in category save path
|
||||||
|
CategoryOptions expandedOptions = options;
|
||||||
|
expandedOptions.savePath = Utils::Fs::expandTilde(options.savePath);
|
||||||
|
if (expandedOptions.downloadPath.has_value())
|
||||||
|
{
|
||||||
|
expandedOptions.downloadPath->path = Utils::Fs::expandTilde(expandedOptions.downloadPath->path);
|
||||||
|
}
|
||||||
|
|
||||||
if (isSubcategoriesEnabled())
|
if (isSubcategoriesEnabled())
|
||||||
{
|
{
|
||||||
for (const QString &parent : asConst(expandCategory(name)))
|
for (const QString &parent : asConst(expandCategory(name)))
|
||||||
|
@ -1018,7 +1026,7 @@ bool SessionImpl::addCategory(const QString &name, const CategoryOptions &option
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_categories[name] = options;
|
m_categories[name] = expandedOptions;
|
||||||
storeCategories();
|
storeCategories();
|
||||||
emit categoryAdded(name);
|
emit categoryAdded(name);
|
||||||
|
|
||||||
|
@ -1031,8 +1039,16 @@ bool SessionImpl::editCategory(const QString &name, const CategoryOptions &optio
|
||||||
if (it == m_categories.end())
|
if (it == m_categories.end())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
// Expand tildes in category save path
|
||||||
|
CategoryOptions expandedOptions = options;
|
||||||
|
expandedOptions.savePath = Utils::Fs::expandTilde(options.savePath);
|
||||||
|
if (expandedOptions.downloadPath.has_value())
|
||||||
|
{
|
||||||
|
expandedOptions.downloadPath->path = Utils::Fs::expandTilde(expandedOptions.downloadPath->path);
|
||||||
|
}
|
||||||
|
|
||||||
CategoryOptions ¤tOptions = it.value();
|
CategoryOptions ¤tOptions = it.value();
|
||||||
if (options == currentOptions)
|
if (expandedOptions == currentOptions)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (isDisableAutoTMMWhenCategorySavePathChanged())
|
if (isDisableAutoTMMWhenCategorySavePathChanged())
|
||||||
|
@ -1047,7 +1063,7 @@ bool SessionImpl::editCategory(const QString &name, const CategoryOptions &optio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentOptions = options;
|
currentOptions = expandedOptions;
|
||||||
storeCategories();
|
storeCategories();
|
||||||
|
|
||||||
for (TorrentImpl *const torrent : asConst(m_torrents))
|
for (TorrentImpl *const torrent : asConst(m_torrents))
|
||||||
|
@ -3281,7 +3297,8 @@ void SessionImpl::removeTorrentsQueue()
|
||||||
|
|
||||||
void SessionImpl::setSavePath(const Path &path)
|
void SessionImpl::setSavePath(const Path &path)
|
||||||
{
|
{
|
||||||
const auto newPath = (path.isAbsolute() ? path : (specialFolderLocation(SpecialFolder::Downloads) / path));
|
const Path expandedPath = Utils::Fs::expandTilde(path);
|
||||||
|
const auto newPath = (expandedPath.isAbsolute() ? expandedPath : (specialFolderLocation(SpecialFolder::Downloads) / expandedPath));
|
||||||
if (newPath == m_savePath)
|
if (newPath == m_savePath)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@ -3321,7 +3338,8 @@ void SessionImpl::setSavePath(const Path &path)
|
||||||
|
|
||||||
void SessionImpl::setDownloadPath(const Path &path)
|
void SessionImpl::setDownloadPath(const Path &path)
|
||||||
{
|
{
|
||||||
const Path newPath = (path.isAbsolute() ? path : (savePath() / Path(u"temp"_s) / path));
|
const Path expandedPath = Utils::Fs::expandTilde(path);
|
||||||
|
const Path newPath = (expandedPath.isAbsolute() ? expandedPath : (savePath() / Path(u"temp"_s) / expandedPath));
|
||||||
if (newPath == m_downloadPath)
|
if (newPath == m_downloadPath)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@ -5581,7 +5599,15 @@ void SessionImpl::loadCategories()
|
||||||
for (auto it = jsonObj.constBegin(); it != jsonObj.constEnd(); ++it)
|
for (auto it = jsonObj.constBegin(); it != jsonObj.constEnd(); ++it)
|
||||||
{
|
{
|
||||||
const QString &categoryName = it.key();
|
const QString &categoryName = it.key();
|
||||||
const auto categoryOptions = CategoryOptions::fromJSON(it.value().toObject());
|
auto categoryOptions = CategoryOptions::fromJSON(it.value().toObject());
|
||||||
|
|
||||||
|
// Expand tildes in loaded category paths
|
||||||
|
categoryOptions.savePath = Utils::Fs::expandTilde(categoryOptions.savePath);
|
||||||
|
if (categoryOptions.downloadPath.has_value())
|
||||||
|
{
|
||||||
|
categoryOptions.downloadPath->path = Utils::Fs::expandTilde(categoryOptions.downloadPath->path);
|
||||||
|
}
|
||||||
|
|
||||||
m_categories[categoryName] = categoryOptions;
|
m_categories[categoryName] = categoryOptions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -219,6 +219,33 @@ Path Utils::Fs::tempPath()
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Path Utils::Fs::expandTilde(const Path &path)
|
||||||
|
{
|
||||||
|
if (path.isEmpty())
|
||||||
|
return path;
|
||||||
|
|
||||||
|
const QString pathStr = path.data();
|
||||||
|
|
||||||
|
// Check if path starts with ~ (tilde)
|
||||||
|
if (pathStr.startsWith(u'~'))
|
||||||
|
{
|
||||||
|
if (pathStr.size() == 1)
|
||||||
|
{
|
||||||
|
// Just ~ means home directory
|
||||||
|
return homePath();
|
||||||
|
}
|
||||||
|
else if (pathStr.at(1) == u'/')
|
||||||
|
{
|
||||||
|
// ~/something means home directory + something
|
||||||
|
return homePath() / Path(pathStr.sliced(2));
|
||||||
|
}
|
||||||
|
// ~username is not supported for simplicity and security
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return original path if no tilde expansion needed
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
bool Utils::Fs::isRegularFile(const Path &path)
|
bool Utils::Fs::isRegularFile(const Path &path)
|
||||||
{
|
{
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
|
|
|
@ -71,4 +71,5 @@ namespace Utils::Fs
|
||||||
|
|
||||||
Path homePath();
|
Path homePath();
|
||||||
Path tempPath();
|
Path tempPath();
|
||||||
|
Path expandTilde(const Path &path);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ set(testFiles
|
||||||
testglobal.cpp
|
testglobal.cpp
|
||||||
testorderedset.cpp
|
testorderedset.cpp
|
||||||
testpath.cpp
|
testpath.cpp
|
||||||
|
testtildeexpansion.cpp
|
||||||
testutilsbytearray.cpp
|
testutilsbytearray.cpp
|
||||||
testutilscompare.cpp
|
testutilscompare.cpp
|
||||||
testutilsdatetime.cpp
|
testutilsdatetime.cpp
|
||||||
|
|
45
test/testtildeexpansion.cpp
Normal file
45
test/testtildeexpansion.cpp
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
#include <QDir>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QTest>
|
||||||
|
|
||||||
|
#include "base/path.h"
|
||||||
|
#include "base/utils/fs.h"
|
||||||
|
|
||||||
|
class TestTildeExpansion : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void testExpandTilde_data();
|
||||||
|
void testExpandTilde();
|
||||||
|
};
|
||||||
|
|
||||||
|
void TestTildeExpansion::testExpandTilde_data()
|
||||||
|
{
|
||||||
|
QTest::addColumn<QString>("input");
|
||||||
|
QTest::addColumn<QString>("expected");
|
||||||
|
|
||||||
|
const QString homeDir = QDir::homePath();
|
||||||
|
|
||||||
|
QTest::newRow("tilde only") << u"~"_s << homeDir;
|
||||||
|
QTest::newRow("tilde with path") << u"~/Downloads"_s << (homeDir + u"/Downloads"_s);
|
||||||
|
QTest::newRow("tilde with nested path") << u"~/Documents/qBittorrent"_s << (homeDir + u"/Documents/qBittorrent"_s);
|
||||||
|
QTest::newRow("absolute path") << u"/absolute/path"_s << u"/absolute/path"_s;
|
||||||
|
QTest::newRow("relative path") << u"relative/path"_s << u"relative/path"_s;
|
||||||
|
QTest::newRow("empty path") << u""_s << u""_s;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestTildeExpansion::testExpandTilde()
|
||||||
|
{
|
||||||
|
QFETCH(QString, input);
|
||||||
|
QFETCH(QString, expected);
|
||||||
|
|
||||||
|
const Path inputPath(input);
|
||||||
|
const Path expandedPath = Utils::Fs::expandTilde(inputPath);
|
||||||
|
|
||||||
|
QCOMPARE(expandedPath.data(), expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
QTEST_APPLESS_MAIN(TestTildeExpansion)
|
||||||
|
#include "testtildeexpansion.moc"
|
Loading…
Add table
Add a link
Reference in a new issue