diff --git a/src/base/bittorrent/sessionimpl.cpp b/src/base/bittorrent/sessionimpl.cpp index a1b665e58..8592e6d56 100644 --- a/src/base/bittorrent/sessionimpl.cpp +++ b/src/base/bittorrent/sessionimpl.cpp @@ -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 ¤tOptions = 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; } } diff --git a/src/base/utils/fs.cpp b/src/base/utils/fs.cpp index d0ac3bbc3..98e6ce98c 100644 --- a/src/base/utils/fs.cpp +++ b/src/base/utils/fs.cpp @@ -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; diff --git a/src/base/utils/fs.h b/src/base/utils/fs.h index eced8b072..cdead3fed 100644 --- a/src/base/utils/fs.h +++ b/src/base/utils/fs.h @@ -71,4 +71,5 @@ namespace Utils::Fs Path homePath(); Path tempPath(); + Path expandTilde(const Path &path); } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 5d8029c05..c1979c36c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -14,6 +14,7 @@ set(testFiles testglobal.cpp testorderedset.cpp testpath.cpp + testtildeexpansion.cpp testutilsbytearray.cpp testutilscompare.cpp testutilsdatetime.cpp diff --git a/test/testtildeexpansion.cpp b/test/testtildeexpansion.cpp new file mode 100644 index 000000000..89c96f7b4 --- /dev/null +++ b/test/testtildeexpansion.cpp @@ -0,0 +1,45 @@ +#include +#include +#include +#include + +#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("input"); + QTest::addColumn("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"