Browse Source

Merge pull request #13995 from glassez/rename-files

Improve content file/folder names handling
adaptive-webui-19844
Vladimir Golovnev 4 years ago committed by GitHub
parent
commit
348109a1f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      src/base/CMakeLists.txt
  2. 2
      src/base/base.pri
  3. 140
      src/base/bittorrent/abstractfilestorage.cpp
  4. 53
      src/base/bittorrent/abstractfilestorage.h
  5. 9
      src/base/bittorrent/torrenthandle.h
  6. 11
      src/base/bittorrent/torrenthandleimpl.cpp
  7. 2
      src/base/bittorrent/torrenthandleimpl.h
  8. 13
      src/base/bittorrent/torrentinfo.h
  9. 2
      src/base/utils/fs.cpp
  10. 4
      src/gui/properties/propertieswidget.cpp
  11. 245
      src/gui/torrentcontenttreeview.cpp
  12. 5
      src/gui/torrentcontenttreeview.h
  13. 54
      src/webui/api/torrentscontroller.cpp
  14. 1
      src/webui/api/torrentscontroller.h
  15. 26
      src/webui/www/private/rename_file.html
  16. 1
      src/webui/www/private/scripts/file-tree.js
  17. 2
      src/webui/www/private/scripts/filesystem.js
  18. 28
      src/webui/www/private/scripts/prop-files.js

2
src/base/CMakeLists.txt

@ -2,6 +2,7 @@ add_library(qbt_base STATIC
# headers # headers
algorithm.h algorithm.h
asyncfilestorage.h asyncfilestorage.h
bittorrent/abstractfilestorage.h
bittorrent/addtorrentparams.h bittorrent/addtorrentparams.h
bittorrent/bandwidthscheduler.h bittorrent/bandwidthscheduler.h
bittorrent/cachestatus.h bittorrent/cachestatus.h
@ -89,6 +90,7 @@ add_library(qbt_base STATIC
# sources # sources
asyncfilestorage.cpp asyncfilestorage.cpp
bittorrent/abstractfilestorage.cpp
bittorrent/bandwidthscheduler.cpp bittorrent/bandwidthscheduler.cpp
bittorrent/customstorage.cpp bittorrent/customstorage.cpp
bittorrent/downloadpriority.cpp bittorrent/downloadpriority.cpp

2
src/base/base.pri

@ -1,6 +1,7 @@
HEADERS += \ HEADERS += \
$$PWD/algorithm.h \ $$PWD/algorithm.h \
$$PWD/asyncfilestorage.h \ $$PWD/asyncfilestorage.h \
$$PWD/bittorrent/abstractfilestorage.h \
$$PWD/bittorrent/addtorrentparams.h \ $$PWD/bittorrent/addtorrentparams.h \
$$PWD/bittorrent/bandwidthscheduler.h \ $$PWD/bittorrent/bandwidthscheduler.h \
$$PWD/bittorrent/cachestatus.h \ $$PWD/bittorrent/cachestatus.h \
@ -89,6 +90,7 @@ HEADERS += \
SOURCES += \ SOURCES += \
$$PWD/asyncfilestorage.cpp \ $$PWD/asyncfilestorage.cpp \
$$PWD/bittorrent/abstractfilestorage.cpp \
$$PWD/bittorrent/bandwidthscheduler.cpp \ $$PWD/bittorrent/bandwidthscheduler.cpp \
$$PWD/bittorrent/customstorage.cpp \ $$PWD/bittorrent/customstorage.cpp \
$$PWD/bittorrent/downloadpriority.cpp \ $$PWD/bittorrent/downloadpriority.cpp \

140
src/base/bittorrent/abstractfilestorage.cpp

@ -0,0 +1,140 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2020 Vladimir Golovnev <glassez@yandex.ru>
*
* 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 "abstractfilestorage.h"
#include <QDir>
#include <QHash>
#include <QVector>
#include "base/bittorrent/common.h"
#include "base/exceptions.h"
#include "base/utils/fs.h"
#if defined(Q_OS_WIN)
const Qt::CaseSensitivity CASE_SENSITIVITY {Qt::CaseInsensitive};
#else
const Qt::CaseSensitivity CASE_SENSITIVITY {Qt::CaseSensitive};
#endif
namespace
{
bool areSameFileNames(QString first, QString second)
{
if (first.endsWith(QB_EXT, Qt::CaseInsensitive))
first.chop(QB_EXT.size());
if (second.endsWith(QB_EXT, Qt::CaseInsensitive))
second.chop(QB_EXT.size());
return QString::compare(first, second, CASE_SENSITIVITY) == 0;
}
}
void BitTorrent::AbstractFileStorage::renameFile(const QString &oldPath, const QString &newPath)
{
if (!Utils::Fs::isValidFileSystemName(oldPath, true))
throw RuntimeError {tr("The old path is invalid: '%1'.").arg(oldPath)};
if (!Utils::Fs::isValidFileSystemName(newPath, true))
throw RuntimeError {tr("The new path is invalid: '%1'.").arg(newPath)};
const QString oldFilePath = Utils::Fs::toUniformPath(oldPath);
if (oldFilePath.endsWith(QLatin1Char {'/'}))
throw RuntimeError {tr("Invalid file path: '%1'.").arg(oldFilePath)};
const QString newFilePath = Utils::Fs::toUniformPath(newPath);
if (newFilePath.endsWith(QLatin1Char {'/'}))
throw RuntimeError {tr("Invalid file path: '%1'.").arg(newFilePath)};
if (QDir().isAbsolutePath(newFilePath))
throw RuntimeError {tr("Absolute path isn't allowed: '%1'.").arg(newFilePath)};
int renamingFileIndex = -1;
for (int i = 0; i < filesCount(); ++i)
{
const QString path = filePath(i);
if ((renamingFileIndex < 0) && areSameFileNames(path, oldFilePath))
renamingFileIndex = i;
if (areSameFileNames(path, newFilePath))
throw RuntimeError {tr("The file already exists: '%1'.").arg(newFilePath)};
}
if (renamingFileIndex < 0)
throw RuntimeError {tr("No such file: '%1'.").arg(oldFilePath)};
const auto extAdjusted = [](const QString &path, const bool needExt) -> QString
{
if (path.endsWith(QB_EXT, Qt::CaseInsensitive) == needExt)
return path;
return (needExt ? (path + QB_EXT) : (path.left(path.size() - QB_EXT.size())));
};
renameFile(renamingFileIndex, extAdjusted(newFilePath, filePath(renamingFileIndex).endsWith(QB_EXT, Qt::CaseInsensitive)));
}
void BitTorrent::AbstractFileStorage::renameFolder(const QString &oldPath, const QString &newPath)
{
if (!Utils::Fs::isValidFileSystemName(oldPath, true))
throw RuntimeError {tr("The old path is invalid: '%1'.").arg(oldPath)};
if (!Utils::Fs::isValidFileSystemName(newPath, true))
throw RuntimeError {tr("The new path is invalid: '%1'.").arg(newPath)};
const auto cleanFolderPath = [](const QString &path) -> QString
{
const QString uniformPath = Utils::Fs::toUniformPath(path);
return (uniformPath.endsWith(QLatin1Char {'/'}) ? uniformPath : uniformPath + QLatin1Char {'/'});
};
const QString oldFolderPath = cleanFolderPath(oldPath);
const QString newFolderPath = cleanFolderPath(newPath);
if (QDir().isAbsolutePath(newFolderPath))
throw RuntimeError {tr("Absolute path isn't allowed: '%1'.").arg(newFolderPath)};
QVector<int> renamingFileIndexes;
renamingFileIndexes.reserve(filesCount());
for (int i = 0; i < filesCount(); ++i)
{
const QString path = filePath(i);
if (path.startsWith(oldFolderPath, CASE_SENSITIVITY))
renamingFileIndexes.append(i);
if (path.startsWith(newFolderPath, CASE_SENSITIVITY))
throw RuntimeError {tr("The folder already exists: '%1'.").arg(newFolderPath)};
}
if (renamingFileIndexes.isEmpty())
throw RuntimeError {tr("No such folder: '%1'.").arg(oldFolderPath)};
for (const int index : renamingFileIndexes)
{
const QString newFilePath = newFolderPath + filePath(index).mid(oldFolderPath.size());
renameFile(index, newFilePath);
}
}

53
src/base/bittorrent/abstractfilestorage.h

@ -0,0 +1,53 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2020 Vladimir Golovnev <glassez@yandex.ru>
*
* 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.
*/
#pragma once
#include <QtGlobal>
#include <QCoreApplication>
class QString;
namespace BitTorrent
{
class AbstractFileStorage
{
Q_DECLARE_TR_FUNCTIONS(AbstractFileStorage)
public:
virtual int filesCount() const = 0;
virtual QString filePath(int index) const = 0;
virtual QString fileName(int index) const = 0;
virtual qlonglong fileSize(int index) const = 0;
virtual void renameFile(int index, const QString &name) = 0;
void renameFile(const QString &oldPath, const QString &newPath);
void renameFolder(const QString &oldPath, const QString &newPath);
};
}

9
src/base/bittorrent/torrenthandle.h

@ -33,6 +33,8 @@
#include <QString> #include <QString>
#include <QtContainerFwd> #include <QtContainerFwd>
#include "abstractfilestorage.h"
class QBitArray; class QBitArray;
class QDateTime; class QDateTime;
class QUrl; class QUrl;
@ -89,7 +91,7 @@ namespace BitTorrent
uint qHash(TorrentState key, uint seed); uint qHash(TorrentState key, uint seed);
class TorrentHandle class TorrentHandle : public AbstractFileStorage
{ {
public: public:
static const qreal USE_GLOBAL_RATIO; static const qreal USE_GLOBAL_RATIO;
@ -177,7 +179,6 @@ namespace BitTorrent
virtual bool removeTag(const QString &tag) = 0; virtual bool removeTag(const QString &tag) = 0;
virtual void removeAllTags() = 0; virtual void removeAllTags() = 0;
virtual int filesCount() const = 0;
virtual int piecesCount() const = 0; virtual int piecesCount() const = 0;
virtual int piecesHave() const = 0; virtual int piecesHave() const = 0;
virtual qreal progress() const = 0; virtual qreal progress() const = 0;
@ -185,9 +186,6 @@ namespace BitTorrent
virtual qreal ratioLimit() const = 0; virtual qreal ratioLimit() const = 0;
virtual int seedingTimeLimit() const = 0; virtual int seedingTimeLimit() const = 0;
virtual QString filePath(int index) const = 0;
virtual QString fileName(int index) const = 0;
virtual qlonglong fileSize(int index) const = 0;
virtual QStringList absoluteFilePaths() const = 0; virtual QStringList absoluteFilePaths() const = 0;
virtual QVector<DownloadPriority> filePriorities() const = 0; virtual QVector<DownloadPriority> filePriorities() const = 0;
@ -273,7 +271,6 @@ namespace BitTorrent
virtual void forceReannounce(int index = -1) = 0; virtual void forceReannounce(int index = -1) = 0;
virtual void forceDHTAnnounce() = 0; virtual void forceDHTAnnounce() = 0;
virtual void forceRecheck() = 0; virtual void forceRecheck() = 0;
virtual void renameFile(int index, const QString &name) = 0;
virtual void prioritizeFiles(const QVector<DownloadPriority> &priorities) = 0; virtual void prioritizeFiles(const QVector<DownloadPriority> &priorities) = 0;
virtual void setRatioLimit(qreal limit) = 0; virtual void setRatioLimit(qreal limit) = 0;
virtual void setSeedingTimeLimit(int limit) = 0; virtual void setSeedingTimeLimit(int limit) = 0;

11
src/base/bittorrent/torrenthandleimpl.cpp

@ -1405,11 +1405,12 @@ void TorrentHandleImpl::moveStorage(const QString &newPath, const MoveStorageMod
} }
} }
void TorrentHandleImpl::renameFile(const int index, const QString &name) void TorrentHandleImpl::renameFile(const int index, const QString &path)
{ {
m_oldPath[lt::file_index_t {index}].push_back(filePath(index)); const QString oldPath = filePath(index);
m_oldPath[lt::file_index_t {index}].push_back(oldPath);
++m_renameCount; ++m_renameCount;
m_nativeHandle.rename_file(lt::file_index_t {index}, Utils::Fs::toNativePath(name).toStdString()); m_nativeHandle.rename_file(lt::file_index_t {index}, Utils::Fs::toNativePath(path).toStdString());
} }
void TorrentHandleImpl::handleStateUpdate(const lt::torrent_status &nativeStatus) void TorrentHandleImpl::handleStateUpdate(const lt::torrent_status &nativeStatus)
@ -1826,7 +1827,7 @@ void TorrentHandleImpl::manageIncompleteFiles()
QString name = filePath(i); QString name = filePath(i);
if (isAppendExtensionEnabled && (fileSize(i) > 0) && (fp[i] < 1)) if (isAppendExtensionEnabled && (fileSize(i) > 0) && (fp[i] < 1))
{ {
if (!name.endsWith(QB_EXT)) if (!name.endsWith(QB_EXT, Qt::CaseInsensitive))
{ {
const QString newName = name + QB_EXT; const QString newName = name + QB_EXT;
qDebug() << "Renaming" << name << "to" << newName; qDebug() << "Renaming" << name << "to" << newName;
@ -1835,7 +1836,7 @@ void TorrentHandleImpl::manageIncompleteFiles()
} }
else else
{ {
if (name.endsWith(QB_EXT)) if (name.endsWith(QB_EXT, Qt::CaseInsensitive))
{ {
const QString oldName = name; const QString oldName = name;
name.chop(QB_EXT.size()); name.chop(QB_EXT.size());

2
src/base/bittorrent/torrenthandleimpl.h

@ -220,7 +220,7 @@ namespace BitTorrent
void forceReannounce(int index = -1) override; void forceReannounce(int index = -1) override;
void forceDHTAnnounce() override; void forceDHTAnnounce() override;
void forceRecheck() override; void forceRecheck() override;
void renameFile(int index, const QString &name) override; void renameFile(int index, const QString &path) override;
void prioritizeFiles(const QVector<DownloadPriority> &priorities) override; void prioritizeFiles(const QVector<DownloadPriority> &priorities) override;
void setRatioLimit(qreal limit) override; void setRatioLimit(qreal limit) override;
void setSeedingTimeLimit(int limit) override; void setSeedingTimeLimit(int limit) override;

13
src/base/bittorrent/torrentinfo.h

@ -34,6 +34,7 @@
#include <QtContainerFwd> #include <QtContainerFwd>
#include "base/indexrange.h" #include "base/indexrange.h"
#include "abstractfilestorage.h"
#include "torrentcontentlayout.h" #include "torrentcontentlayout.h"
class QByteArray; class QByteArray;
@ -46,7 +47,7 @@ namespace BitTorrent
class InfoHash; class InfoHash;
class TrackerEntry; class TrackerEntry;
class TorrentInfo class TorrentInfo final : public AbstractFileStorage
{ {
Q_DECLARE_TR_FUNCTIONS(TorrentInfo) Q_DECLARE_TR_FUNCTIONS(TorrentInfo)
@ -68,15 +69,15 @@ namespace BitTorrent
QString comment() const; QString comment() const;
bool isPrivate() const; bool isPrivate() const;
qlonglong totalSize() const; qlonglong totalSize() const;
int filesCount() const; int filesCount() const override;
int pieceLength() const; int pieceLength() const;
int pieceLength(int index) const; int pieceLength(int index) const;
int piecesCount() const; int piecesCount() const;
QString filePath(int index) const; QString filePath(int index) const override;
QStringList filePaths() const; QStringList filePaths() const;
QString fileName(int index) const; QString fileName(int index) const override;
QString origFilePath(int index) const; QString origFilePath(int index) const;
qlonglong fileSize(int index) const; qlonglong fileSize(int index) const override;
qlonglong fileOffset(int index) const; qlonglong fileOffset(int index) const;
QVector<TrackerEntry> trackers() const; QVector<TrackerEntry> trackers() const;
QVector<QUrl> urlSeeds() const; QVector<QUrl> urlSeeds() const;
@ -91,7 +92,7 @@ namespace BitTorrent
PieceRange filePieces(const QString &file) const; PieceRange filePieces(const QString &file) const;
PieceRange filePieces(int fileIndex) const; PieceRange filePieces(int fileIndex) const;
void renameFile(int index, const QString &newPath); void renameFile(int index, const QString &newPath) override;
QString rootFolder() const; QString rootFolder() const;
bool hasRootFolder() const; bool hasRootFolder() const;

2
src/base/utils/fs.cpp

@ -95,7 +95,7 @@ QString Utils::Fs::folderName(const QString &filePath)
const QString path = toUniformPath(filePath); const QString path = toUniformPath(filePath);
const int slashIndex = path.lastIndexOf('/'); const int slashIndex = path.lastIndexOf('/');
if (slashIndex == -1) if (slashIndex == -1)
return path; return {};
return path.left(slashIndex); return path.left(slashIndex);
} }

4
src/gui/properties/propertieswidget.cpp

@ -158,7 +158,7 @@ PropertiesWidget::PropertiesWidget(QWidget *parent)
connect(m_ui->listWebSeeds, &QListWidget::doubleClicked, this, &PropertiesWidget::editWebSeed); connect(m_ui->listWebSeeds, &QListWidget::doubleClicked, this, &PropertiesWidget::editWebSeed);
const auto *renameFileHotkey = new QShortcut(Qt::Key_F2, m_ui->filesList, nullptr, nullptr, Qt::WidgetShortcut); const auto *renameFileHotkey = new QShortcut(Qt::Key_F2, m_ui->filesList, nullptr, nullptr, Qt::WidgetShortcut);
connect(renameFileHotkey, &QShortcut::activated, this, [this]() { m_ui->filesList->renameSelectedFile(m_torrent); }); connect(renameFileHotkey, &QShortcut::activated, this, [this]() { m_ui->filesList->renameSelectedFile(*m_torrent); });
const auto *openFileHotkeyReturn = new QShortcut(Qt::Key_Return, m_ui->filesList, nullptr, nullptr, Qt::WidgetShortcut); const auto *openFileHotkeyReturn = new QShortcut(Qt::Key_Return, m_ui->filesList, nullptr, nullptr, Qt::WidgetShortcut);
connect(openFileHotkeyReturn, &QShortcut::activated, this, &PropertiesWidget::openSelectedFile); connect(openFileHotkeyReturn, &QShortcut::activated, this, &PropertiesWidget::openSelectedFile);
const auto *openFileHotkeyEnter = new QShortcut(Qt::Key_Enter, m_ui->filesList, nullptr, nullptr, Qt::WidgetShortcut); const auto *openFileHotkeyEnter = new QShortcut(Qt::Key_Enter, m_ui->filesList, nullptr, nullptr, Qt::WidgetShortcut);
@ -593,7 +593,7 @@ void PropertiesWidget::displayFilesListMenu(const QPoint &)
connect(actOpenContainingFolder, &QAction::triggered, this, [this, index]() { openParentFolder(index); }); connect(actOpenContainingFolder, &QAction::triggered, this, [this, index]() { openParentFolder(index); });
const QAction *actRename = menu->addAction(UIThemeManager::instance()->getIcon("edit-rename"), tr("Rename...")); const QAction *actRename = menu->addAction(UIThemeManager::instance()->getIcon("edit-rename"), tr("Rename..."));
connect(actRename, &QAction::triggered, this, [this]() { m_ui->filesList->renameSelectedFile(m_torrent); }); connect(actRename, &QAction::triggered, this, [this]() { m_ui->filesList->renameSelectedFile(*m_torrent); });
menu->addSeparator(); menu->addSeparator();
} }

245
src/gui/torrentcontenttreeview.cpp

@ -37,10 +37,12 @@
#include <QTableView> #include <QTableView>
#include <QThread> #include <QThread>
#include "base/bittorrent/abstractfilestorage.h"
#include "base/bittorrent/common.h" #include "base/bittorrent/common.h"
#include "base/bittorrent/session.h" #include "base/bittorrent/session.h"
#include "base/bittorrent/torrenthandle.h" #include "base/bittorrent/torrenthandle.h"
#include "base/bittorrent/torrentinfo.h" #include "base/bittorrent/torrentinfo.h"
#include "base/exceptions.h"
#include "base/global.h" #include "base/global.h"
#include "base/utils/fs.h" #include "base/utils/fs.h"
#include "autoexpandabledialog.h" #include "autoexpandabledialog.h"
@ -48,6 +50,17 @@
#include "torrentcontentfiltermodel.h" #include "torrentcontentfiltermodel.h"
#include "torrentcontentmodelitem.h" #include "torrentcontentmodelitem.h"
namespace
{
QString getFullPath(const QModelIndex &idx)
{
QStringList paths;
for (QModelIndex i = idx; i.isValid(); i = i.parent())
paths.prepend(i.data().toString());
return paths.join(QLatin1Char {'/'});
}
}
TorrentContentTreeView::TorrentContentTreeView(QWidget *parent) TorrentContentTreeView::TorrentContentTreeView(QWidget *parent)
: QTreeView(parent) : QTreeView(parent)
{ {
@ -91,10 +104,8 @@ void TorrentContentTreeView::keyPressEvent(QKeyEvent *event)
} }
} }
void TorrentContentTreeView::renameSelectedFile(BitTorrent::TorrentHandle *torrent) void TorrentContentTreeView::renameSelectedFile(BitTorrent::AbstractFileStorage &fileStorage)
{ {
if (!torrent) return;
const QModelIndexList selectedIndexes = selectionModel()->selectedRows(0); const QModelIndexList selectedIndexes = selectionModel()->selectedRows(0);
if (selectedIndexes.size() != 1) return; if (selectedIndexes.size() != 1) return;
@ -112,228 +123,26 @@ void TorrentContentTreeView::renameSelectedFile(BitTorrent::TorrentHandle *torre
, modelIndex.data().toString(), &ok, isFile).trimmed(); , modelIndex.data().toString(), &ok, isFile).trimmed();
if (!ok || !modelIndex.isValid()) return; if (!ok || !modelIndex.isValid()) return;
if (!Utils::Fs::isValidFileSystemName(newName)) const QString oldName = modelIndex.data().toString();
{ if (newName == oldName)
RaisedMessageBox::warning(this, tr("Rename error"), return; // Name did not change
tr("The name is empty or contains forbidden characters, please choose a different one."),
QMessageBox::Ok);
return;
}
if (isFile) const QString parentPath = getFullPath(modelIndex.parent());
{ const QString oldPath {parentPath.isEmpty() ? oldName : parentPath + QLatin1Char {'/'} + oldName};
const int fileIndex = model->getFileIndex(modelIndex); const QString newPath {parentPath.isEmpty() ? newName : parentPath + QLatin1Char {'/'} + newName};
if (newName.endsWith(QB_EXT)) try
newName.chop(QB_EXT.size());
const QString oldFileName = torrent->fileName(fileIndex);
const QString oldFilePath = torrent->filePath(fileIndex);
const bool useFilenameExt = BitTorrent::Session::instance()->isAppendExtensionEnabled()
&& (torrent->filesProgress()[fileIndex] != 1);
const QString newFileName = newName + (useFilenameExt ? QB_EXT : QString());
const QString newFilePath = oldFilePath.leftRef(oldFilePath.size() - oldFileName.size()) + newFileName;
if (oldFileName == newFileName)
{
qDebug("Name did not change: %s", qUtf8Printable(oldFileName));
return;
}
// check if that name is already used
for (int i = 0; i < torrent->filesCount(); ++i)
{
if (i == fileIndex) continue;
if (Utils::Fs::sameFileNames(torrent->filePath(i), newFilePath))
{
RaisedMessageBox::warning(this, tr("Rename error"),
tr("This name is already in use in this folder. Please use a different name."),
QMessageBox::Ok);
return;
}
}
qDebug("Renaming %s to %s", qUtf8Printable(oldFilePath), qUtf8Printable(newFilePath));
torrent->renameFile(fileIndex, newFilePath);
model->setData(modelIndex, newName);
}
else
{ {
// renaming a folder if (isFile)
fileStorage.renameFile(oldPath, newPath);
const QString oldName = modelIndex.data().toString(); else
if (newName == oldName) fileStorage.renameFolder(oldPath, newPath);
return; // Name did not change
QString parentPath;
for (QModelIndex idx = model->parent(modelIndex); idx.isValid(); idx = model->parent(idx))
parentPath.prepend(idx.data().toString() + '/');
const QString oldPath {parentPath + oldName + '/'};
const QString newPath {parentPath + newName + '/'};
// Check for overwriting
#if defined(Q_OS_WIN)
const Qt::CaseSensitivity caseSensitivity = Qt::CaseInsensitive;
#else
const Qt::CaseSensitivity caseSensitivity = Qt::CaseSensitive;
#endif
for (int i = 0; i < torrent->filesCount(); ++i)
{
const QString currentPath = torrent->filePath(i);
if (currentPath.startsWith(oldPath))
continue;
if (currentPath.startsWith(newPath, caseSensitivity))
{
RaisedMessageBox::warning(this, tr("The folder could not be renamed"),
tr("This name is already in use. Please use a different name."),
QMessageBox::Ok);
return;
}
}
// Replace path in all files
bool needForceRecheck = false;
for (int i = 0; i < torrent->filesCount(); ++i)
{
const QString currentPath = torrent->filePath(i);
if (currentPath.startsWith(oldPath))
{
const QString path {newPath + currentPath.mid(oldPath.length())};
if (!needForceRecheck && QFile::exists(path))
needForceRecheck = true;
torrent->renameFile(i, path);
}
}
// Force recheck
if (needForceRecheck)
torrent->forceRecheck();
model->setData(modelIndex, newName); model->setData(modelIndex, newName);
} }
} catch (const RuntimeError &error)
void TorrentContentTreeView::renameSelectedFile(BitTorrent::TorrentInfo &torrent)
{
const QModelIndexList selectedIndexes = selectionModel()->selectedRows(0);
if (selectedIndexes.size() != 1) return;
const QPersistentModelIndex modelIndex = selectedIndexes.first();
if (!modelIndex.isValid()) return;
auto model = dynamic_cast<TorrentContentFilterModel *>(TorrentContentTreeView::model());
if (!model) return;
const bool isFile = (model->itemType(modelIndex) == TorrentContentModelItem::FileType);
// Ask for new name
bool ok = false;
QString newName = AutoExpandableDialog::getText(this, tr("Renaming"), tr("New name:"), QLineEdit::Normal
, modelIndex.data().toString(), &ok, isFile).trimmed();
if (!ok || !modelIndex.isValid()) return;
if (!Utils::Fs::isValidFileSystemName(newName))
{
RaisedMessageBox::warning(this, tr("Rename error"),
tr("The name is empty or contains forbidden characters, please choose a different one."),
QMessageBox::Ok);
return;
}
if (isFile)
{
const int fileIndex = model->getFileIndex(modelIndex);
if (newName.endsWith(QB_EXT))
newName.chop(QB_EXT.size());
const QString oldFileName = torrent.fileName(fileIndex);
const QString oldFilePath = torrent.filePath(fileIndex);
const QString newFilePath = oldFilePath.leftRef(oldFilePath.size() - oldFileName.size()) + newName;
if (oldFileName == newName)
{
qDebug("Name did not change: %s", qUtf8Printable(oldFileName));
return;
}
// check if that name is already used
for (int i = 0; i < torrent.filesCount(); ++i)
{
if (i == fileIndex) continue;
if (Utils::Fs::sameFileNames(torrent.filePath(i), newFilePath))
{
RaisedMessageBox::warning(this, tr("Rename error"),
tr("This name is already in use in this folder. Please use a different name."),
QMessageBox::Ok);
return;
}
}
qDebug("Renaming %s to %s", qUtf8Printable(oldFilePath), qUtf8Printable(newFilePath));
torrent.renameFile(fileIndex, newFilePath);
model->setData(modelIndex, newName);
}
else
{ {
// renaming a folder RaisedMessageBox::warning(this, tr("Rename error"), error.message(), QMessageBox::Ok);
const QString oldName = modelIndex.data().toString();
if (newName == oldName)
return; // Name did not change
QString parentPath;
for (QModelIndex idx = model->parent(modelIndex); idx.isValid(); idx = model->parent(idx))
parentPath.prepend(idx.data().toString() + '/');
const QString oldPath {parentPath + oldName + '/'};
const QString newPath {parentPath + newName + '/'};
// Check for overwriting
#if defined(Q_OS_WIN)
const Qt::CaseSensitivity caseSensitivity = Qt::CaseInsensitive;
#else
const Qt::CaseSensitivity caseSensitivity = Qt::CaseSensitive;
#endif
for (int i = 0; i < torrent.filesCount(); ++i)
{
const QString currentPath = torrent.filePath(i);
if (currentPath.startsWith(oldPath))
continue;
if (currentPath.startsWith(newPath, caseSensitivity))
{
RaisedMessageBox::warning(this, tr("The folder could not be renamed"),
tr("This name is already in use. Please use a different name."),
QMessageBox::Ok);
return;
}
}
// Replace path in all files
for (int i = 0; i < torrent.filesCount(); ++i)
{
const QString currentPath = torrent.filePath(i);
if (currentPath.startsWith(oldPath))
{
const QString path {newPath + currentPath.mid(oldPath.length())};
torrent.renameFile(i, path);
}
}
model->setData(modelIndex, newName);
} }
} }

5
src/gui/torrentcontenttreeview.h

@ -32,6 +32,7 @@
namespace BitTorrent namespace BitTorrent
{ {
class AbstractFileStorage;
class TorrentHandle; class TorrentHandle;
class TorrentInfo; class TorrentInfo;
} }
@ -39,13 +40,13 @@ namespace BitTorrent
class TorrentContentTreeView final : public QTreeView class TorrentContentTreeView final : public QTreeView
{ {
Q_OBJECT Q_OBJECT
Q_DISABLE_COPY(TorrentContentTreeView)
public: public:
explicit TorrentContentTreeView(QWidget *parent = nullptr); explicit TorrentContentTreeView(QWidget *parent = nullptr);
void keyPressEvent(QKeyEvent *event) override; void keyPressEvent(QKeyEvent *event) override;
void renameSelectedFile(BitTorrent::TorrentHandle *torrent); void renameSelectedFile(BitTorrent::AbstractFileStorage &fileStorage);
void renameSelectedFile(BitTorrent::TorrentInfo &torrent);
private: private:
QModelIndex currentNameCell(); QModelIndex currentNameCell();

54
src/webui/api/torrentscontroller.cpp

@ -1251,44 +1251,44 @@ void TorrentsController::tagsAction()
void TorrentsController::renameFileAction() void TorrentsController::renameFileAction()
{ {
requireParams({"hash", "id", "name"}); requireParams({"hash", "oldPath", "newPath"});
const QString hash = params()["hash"]; const QString hash = params()["hash"];
BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash);
if (!torrent) if (!torrent)
throw APIError(APIErrorType::NotFound); throw APIError(APIErrorType::NotFound);
QString newName = params()["name"].trimmed(); const QString oldPath = params()["oldPath"];
if (newName.isEmpty()) const QString newPath = params()["newPath"];
throw APIError(APIErrorType::BadParams, tr("Name cannot be empty"));
if (!Utils::Fs::isValidFileSystemName(newName))
throw APIError(APIErrorType::Conflict, tr("Name is not valid"));
if (newName.endsWith(QB_EXT))
newName.chop(QB_EXT.size());
bool ok = false; try
const int fileIndex = params()["id"].toInt(&ok); {
if (!ok || (fileIndex < 0) || (fileIndex >= torrent->filesCount())) torrent->renameFile(oldPath, newPath);
throw APIError(APIErrorType::Conflict, tr("ID is not valid")); }
catch (const RuntimeError &error)
{
throw APIError(APIErrorType::Conflict, error.message());
}
}
const QString oldFileName = torrent->fileName(fileIndex); void TorrentsController::renameFolderAction()
const QString oldFilePath = torrent->filePath(fileIndex); {
requireParams({"hash", "oldPath", "newPath"});
const bool useFilenameExt = BitTorrent::Session::instance()->isAppendExtensionEnabled() const QString hash = params()["hash"];
&& (torrent->filesProgress()[fileIndex] != 1); BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash);
const QString newFileName = (newName + (useFilenameExt ? QB_EXT : QString())); if (!torrent)
const QString newFilePath = (oldFilePath.leftRef(oldFilePath.size() - oldFileName.size()) + newFileName); throw APIError(APIErrorType::NotFound);
if (oldFileName == newFileName) const QString oldPath = params()["oldPath"];
return; const QString newPath = params()["newPath"];
// check if new name is already used try
for (int i = 0; i < torrent->filesCount(); ++i)
{ {
if (i == fileIndex) continue; torrent->renameFolder(oldPath, newPath);
if (Utils::Fs::sameFileNames(torrent->filePath(i), newFilePath)) }
throw APIError(APIErrorType::Conflict, tr("Name is already in use")); catch (const RuntimeError &error)
{
throw APIError(APIErrorType::Conflict, error.message());
} }
torrent->renameFile(fileIndex, newFilePath);
} }

1
src/webui/api/torrentscontroller.h

@ -84,4 +84,5 @@ private slots:
void toggleSequentialDownloadAction(); void toggleSequentialDownloadAction();
void toggleFirstLastPiecePrioAction(); void toggleFirstLastPiecePrioAction();
void renameFileAction(); void renameFileAction();
void renameFolderAction();
}; };

26
src/webui/www/private/rename_file.html

@ -8,6 +8,7 @@
<script src="scripts/lib/mootools-1.2-core-yc.js"></script> <script src="scripts/lib/mootools-1.2-core-yc.js"></script>
<script src="scripts/lib/mootools-1.2-more.js"></script> <script src="scripts/lib/mootools-1.2-more.js"></script>
<script src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script> <script src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script>
<script src="scripts/filesystem.js?v=${CACHEID}"></script>
<script> <script>
'use strict'; 'use strict';
@ -31,14 +32,15 @@
window.addEvent('domready', function() { window.addEvent('domready', function() {
const hash = new URI().getData('hash'); const hash = new URI().getData('hash');
const name = new URI().getData('name'); const path = new URI().getData('path');
const id = new URI().getData('id'); const isFolder = ((new URI().getData('isFolder')) === 'true');
if (!hash || !name || !id) return;
const decodedName = decodeURIComponent(name); const oldPath = decodeURIComponent(path);
$('rename').value = decodedName; const oldName = window.qBittorrent.Filesystem.fileName(oldPath);
$('rename').value = oldName;
$('rename').focus(); $('rename').focus();
$('rename').setSelectionRange(0, decodedName.lastIndexOf('.')); if (!isFolder)
$('rename').setSelectionRange(0, oldName.lastIndexOf('.'));
$('renameButton').addEvent('click', function(e) { $('renameButton').addEvent('click', function(e) {
new Event(e).stop(); new Event(e).stop();
@ -49,20 +51,24 @@
return; return;
} }
if (newName === name) { if (newName === oldName) {
alert('QBT_TR(Name is unchanged)QBT_TR[CONTEXT=HttpServer]'); alert('QBT_TR(Name is unchanged)QBT_TR[CONTEXT=HttpServer]');
return; return;
} }
$('renameButton').disabled = true; $('renameButton').disabled = true;
const parentPath = window.qBittorrent.Filesystem.folderName(oldPath)
const newPath = parentPath
? parentPath + window.qBittorrent.Filesystem.PathSeparator + newName
: newName;
new Request({ new Request({
url: 'api/v2/torrents/renameFile', url: isFolder ? 'api/v2/torrents/renameFolder' : 'api/v2/torrents/renameFile',
method: 'post', method: 'post',
data: { data: {
hash: hash, hash: hash,
id: id, oldPath: oldPath,
name: newName newPath: newPath
}, },
onSuccess: function() { onSuccess: function() {
window.parent.closeWindows(); window.parent.closeWindows();

1
src/webui/www/private/scripts/file-tree.js

@ -117,6 +117,7 @@ window.qBittorrent.FileTree = (function() {
const FileNode = new Class({ const FileNode = new Class({
name: "", name: "",
path: "",
rowId: null, rowId: null,
size: 0, size: 0,
checked: TriState.Unchecked, checked: TriState.Unchecked,

2
src/webui/www/private/scripts/filesystem.js

@ -70,7 +70,7 @@ window.qBittorrent.Filesystem = (function() {
const folderName = function(filepath) { const folderName = function(filepath) {
const slashIndex = filepath.lastIndexOf(PathSeparator); const slashIndex = filepath.lastIndexOf(PathSeparator);
if (slashIndex === -1) if (slashIndex === -1)
return filepath; return '';
return filepath.substring(0, slashIndex); return filepath.substring(0, slashIndex);
}; };

28
src/webui/www/private/scripts/prop-files.js

@ -423,9 +423,9 @@ window.qBittorrent.PropFiles = (function() {
rows.forEach(function(row) { rows.forEach(function(row) {
let parent = rootNode; let parent = rootNode;
const pathFolders = row.fileName.split(window.qBittorrent.Filesystem.PathSeparator); let folderPath = window.qBittorrent.Filesystem.folderName(row.fileName);
pathFolders.pop(); while (folderPath) {
pathFolders.forEach(function(folderName) { const folderName = window.qBittorrent.Filesystem.fileName(folderPath);
if (folderName === '.unwanted') if (folderName === '.unwanted')
return; return;
@ -439,8 +439,10 @@ window.qBittorrent.PropFiles = (function() {
} }
} }
} }
if (parentNode === null) { if (parentNode === null) {
parentNode = new window.qBittorrent.FileTree.FolderNode(); parentNode = new window.qBittorrent.FileTree.FolderNode();
parentNode.path = folderPath;
parentNode.name = folderName; parentNode.name = folderName;
parentNode.rowId = rowId; parentNode.rowId = rowId;
parentNode.root = parent; parentNode.root = parent;
@ -450,12 +452,14 @@ window.qBittorrent.PropFiles = (function() {
} }
parent = parentNode; parent = parentNode;
}); folderPath = window.qBittorrent.Filesystem.folderName(folderPath);
}
const isChecked = row.checked ? TriState.Checked : TriState.Unchecked; const isChecked = row.checked ? TriState.Checked : TriState.Unchecked;
const remaining = (row.priority === FilePriority.Ignored) ? 0 : row.remaining; const remaining = (row.priority === FilePriority.Ignored) ? 0 : row.remaining;
const childNode = new window.qBittorrent.FileTree.FileNode(); const childNode = new window.qBittorrent.FileTree.FileNode();
childNode.name = row.name; childNode.name = row.name;
childNode.path = row.fileName;
childNode.rowId = rowId; childNode.rowId = rowId;
childNode.size = row.size; childNode.size = row.size;
childNode.checked = isChecked; childNode.checked = isChecked;
@ -527,17 +531,16 @@ window.qBittorrent.PropFiles = (function() {
if (rowId === undefined) return; if (rowId === undefined) return;
const row = torrentFilesTable.rows[rowId]; const row = torrentFilesTable.rows[rowId];
if (!row) return; if (!row) return;
const node = torrentFilesTable.getNode(rowId);
if (node.isFolder) return;
const name = row.full_data.name; const node = torrentFilesTable.getNode(rowId);
const fileId = row.full_data.fileId; const path = node.path;
new MochaUI.Window({ new MochaUI.Window({
id: 'renamePage', id: 'renamePage',
title: "QBT_TR(Renaming)QBT_TR[CONTEXT=TorrentContentTreeView]", title: "QBT_TR(Renaming)QBT_TR[CONTEXT=TorrentContentTreeView]",
loadMethod: 'iframe', loadMethod: 'iframe',
contentURL: 'rename_file.html?hash=' + hash + '&id=' + fileId + '&name=' + encodeURIComponent(name), contentURL: 'rename_file.html?hash=' + hash + '&isFolder=' + node.isFolder
+ '&path=' + encodeURIComponent(path),
scrollbars: false, scrollbars: false,
resizable: false, resizable: false,
maximizable: false, maximizable: false,
@ -570,13 +573,6 @@ window.qBittorrent.PropFiles = (function() {
this.hideItem('FilePrio'); this.hideItem('FilePrio');
else else
this.showItem('FilePrio'); this.showItem('FilePrio');
const rowId = torrentFilesTable.selectedRowsIds()[0];
const node = torrentFilesTable.getNode(rowId);
if (node.isFolder)
this.hideItem('Rename');
else
this.showItem('Rename');
} }
}); });

Loading…
Cancel
Save