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:
ExcitedHumvee 2025-08-15 01:37:06 +05:30
commit 242d33ab53
5 changed files with 108 additions and 8 deletions

View file

@ -552,8 +552,8 @@ SessionImpl::SessionImpl(QObject *parent)
, m_storedTags(BITTORRENT_SESSION_KEY(u"Tags"_s))
, m_shareLimitAction(BITTORRENT_SESSION_KEY(u"ShareLimitAction"_s), ShareLimitAction::Stop
, [](const ShareLimitAction action) { return (action == ShareLimitAction::Default) ? ShareLimitAction::Stop : action; })
, m_savePath(BITTORRENT_SESSION_KEY(u"DefaultSavePath"_s), specialFolderLocation(SpecialFolder::Downloads))
, m_downloadPath(BITTORRENT_SESSION_KEY(u"TempPath"_s), (savePath() / Path(u"temp"_s)))
, 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)), Utils::Fs::expandTilde)
, m_isDownloadPathEnabled(BITTORRENT_SESSION_KEY(u"TempPathEnabled"_s), false)
, m_isSubcategoriesEnabled(BITTORRENT_SESSION_KEY(u"SubcategoriesEnabled"_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))
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())
{
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();
emit categoryAdded(name);
@ -1031,8 +1039,16 @@ bool SessionImpl::editCategory(const QString &name, const CategoryOptions &optio
if (it == m_categories.end())
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 &currentOptions = it.value();
if (options == currentOptions)
if (expandedOptions == currentOptions)
return false;
if (isDisableAutoTMMWhenCategorySavePathChanged())
@ -1047,7 +1063,7 @@ bool SessionImpl::editCategory(const QString &name, const CategoryOptions &optio
}
}
currentOptions = options;
currentOptions = expandedOptions;
storeCategories();
for (TorrentImpl *const torrent : asConst(m_torrents))
@ -3281,7 +3297,8 @@ void SessionImpl::removeTorrentsQueue()
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)
return;
@ -3321,7 +3338,8 @@ void SessionImpl::setSavePath(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)
return;
@ -5581,7 +5599,15 @@ void SessionImpl::loadCategories()
for (auto it = jsonObj.constBegin(); it != jsonObj.constEnd(); ++it)
{
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;
}
}

View file

@ -219,6 +219,33 @@ Path Utils::Fs::tempPath()
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)
{
std::error_code ec;

View file

@ -71,4 +71,5 @@ namespace Utils::Fs
Path homePath();
Path tempPath();
Path expandTilde(const Path &path);
}

View file

@ -14,6 +14,7 @@ set(testFiles
testglobal.cpp
testorderedset.cpp
testpath.cpp
testtildeexpansion.cpp
testutilsbytearray.cpp
testutilscompare.cpp
testutilsdatetime.cpp

View 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"