This commit is contained in:
ExcitedHumvee 2025-08-18 03:08:06 +08:00 committed by GitHub
commit 4f39e28a06
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 141 additions and 8 deletions

View file

@ -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 &currentOptions = it.value(); CategoryOptions &currentOptions = 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;
} }
} }

View file

@ -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;

View file

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

View file

@ -17,6 +17,7 @@ set(testFiles
testutilsbytearray.cpp testutilsbytearray.cpp
testutilscompare.cpp testutilscompare.cpp
testutilsdatetime.cpp testutilsdatetime.cpp
testutilsfs.cpp
testutilsgzip.cpp testutilsgzip.cpp
testutilsio.cpp testutilsio.cpp
testutilsnumber.cpp testutilsnumber.cpp

78
test/testutilsfs.cpp Normal file
View file

@ -0,0 +1,78 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2025 ExcitedHumvee
*
* 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 <QDir>
#include <QObject>
#include <QString>
#include <QTest>
#include "base/global.h"
#include "base/path.h"
#include "base/utils/fs.h"
class TestUtilsFs final : public QObject
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(TestUtilsFs)
public:
TestUtilsFs() = default;
private slots:
void testExpandTilde_data() const;
void testExpandTilde() const;
};
void TestUtilsFs::testExpandTilde_data() const
{
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 << QString(homeDir + u"/Downloads"_s);
QTest::newRow("tilde with nested path") << u"~/Documents/qBittorrent"_s << QString(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 TestUtilsFs::testExpandTilde() const
{
QFETCH(QString, input);
QFETCH(QString, expected);
const Path inputPath(input);
const Path expandedPath = Utils::Fs::expandTilde(inputPath);
QCOMPARE(expandedPath.data(), expected);
}
QTEST_APPLESS_MAIN(TestUtilsFs)
#include "testutilsfs.moc"