feat: warnings when instance resources are linked

Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com>
This commit is contained in:
Rachel Powers 2023-02-12 02:44:39 -07:00
parent 9f441a9678
commit a1053a4c5a
13 changed files with 179 additions and 3 deletions

View File

@ -1641,5 +1641,9 @@ bool canLink(const QString& src, const QString& dst)
return canLinkOnFS(src) && canLinkOnFS(dst); return canLinkOnFS(src) && canLinkOnFS(dst);
} }
uintmax_t hardLinkCount(const QString& path)
{
return fs::hard_link_count(StringUtils::toStdString(path));
}
} }

View File

@ -528,4 +528,6 @@ bool canLinkOnFS(FilesystemType type);
*/ */
bool canLink(const QString& src, const QString& dst); bool canLink(const QString& src, const QString& dst);
uintmax_t hardLinkCount(const QString& path);
} }

View File

@ -56,6 +56,8 @@
#include <optional> #include <optional>
#include "FileSystem.h"
using std::optional; using std::optional;
using std::nullopt; using std::nullopt;
@ -567,3 +569,25 @@ bool World::operator==(const World &other) const
{ {
return is_valid == other.is_valid && folderName() == other.folderName(); return is_valid == other.is_valid && folderName() == other.folderName();
} }
bool World::isSymLinkUnder(const QString& instPath) const
{
if (isSymLink())
return true;
auto instDir = QDir(instPath);
auto relAbsPath = instDir.relativeFilePath(m_containerFile.absoluteFilePath());
auto relCanonPath = instDir.relativeFilePath(m_containerFile.canonicalFilePath());
return relAbsPath != relCanonPath;
}
bool World::isMoreThanOneHardLink() const
{
if (m_containerFile.isDir())
{
return FS::hardLinkCount(QDir(m_containerFile.absoluteFilePath()).filePath("level.dat")) > 1;
}
return FS::hardLinkCount(m_containerFile.absoluteFilePath()) > 1;
}

View File

@ -95,6 +95,21 @@ public:
// WEAK compare operator - used for replacing worlds // WEAK compare operator - used for replacing worlds
bool operator==(const World &other) const; bool operator==(const World &other) const;
[[nodiscard]] auto isSymLink() const -> bool{ return m_containerFile.isSymLink(); }
/**
* @brief Take a instance path, checks if the file pointed to by the resource is a symlink or under a symlink in that instance
*
* @param instPath path to an instance directory
* @return true
* @return false
*/
[[nodiscard]] bool isSymLinkUnder(const QString& instPath) const;
[[nodiscard]] bool isMoreThanOneHardLink() const;
QString canonicalFilePath() const { return m_containerFile.canonicalFilePath(); }
private: private:
void readFromZip(const QFileInfo &file); void readFromZip(const QFileInfo &file);
void readFromFS(const QFileInfo &file); void readFromFS(const QFileInfo &file);

View File

@ -128,6 +128,10 @@ bool WorldList::isValid()
return m_dir.exists() && m_dir.isReadable(); return m_dir.exists() && m_dir.isReadable();
} }
QString WorldList::instDirPath() const {
return QFileInfo(m_dir.filePath("../..")).absoluteFilePath();
}
bool WorldList::deleteWorld(int index) bool WorldList::deleteWorld(int index)
{ {
if (index >= worlds.size() || index < 0) if (index >= worlds.size() || index < 0)
@ -173,7 +177,7 @@ bool WorldList::resetIcon(int row)
int WorldList::columnCount(const QModelIndex &parent) const int WorldList::columnCount(const QModelIndex &parent) const
{ {
return parent.isValid()? 0 : 4; return parent.isValid()? 0 : 5;
} }
QVariant WorldList::data(const QModelIndex &index, int role) const QVariant WorldList::data(const QModelIndex &index, int role) const
@ -207,6 +211,14 @@ QVariant WorldList::data(const QModelIndex &index, int role) const
case SizeColumn: case SizeColumn:
return locale.formattedDataSize(world.bytes()); return locale.formattedDataSize(world.bytes());
case InfoColumn:
if (world.isSymLinkUnder(instDirPath())) {
return tr("This world is symbolicly linked from elsewhere.");
}
if (world.isMoreThanOneHardLink()) {
return tr("\nThis world is hard linked elsewhere.");
}
return "";
default: default:
return QVariant(); return QVariant();
} }
@ -222,7 +234,16 @@ QVariant WorldList::data(const QModelIndex &index, int role) const
} }
case Qt::ToolTipRole: case Qt::ToolTipRole:
{ {
if (column == InfoColumn) {
if (world.isSymLinkUnder(instDirPath())) {
return tr("Warning: This world is symbolicly linked from elsewhere. Editing it will also change the origonal") +
tr("\nCanonical Path: %1").arg(world.canonicalFilePath());
}
if (world.isMoreThanOneHardLink()) {
return tr("Warning: This world is hard linked elsewhere. Editing it will also change the origonal");
}
}
return world.folderName(); return world.folderName();
} }
case ObjectRole: case ObjectRole:
@ -274,6 +295,9 @@ QVariant WorldList::headerData(int section, Qt::Orientation orientation, int rol
case SizeColumn: case SizeColumn:
//: World size on disk //: World size on disk
return tr("Size"); return tr("Size");
case InfoColumn:
//: special warnings?
return tr("Info");
default: default:
return QVariant(); return QVariant();
} }
@ -289,6 +313,8 @@ QVariant WorldList::headerData(int section, Qt::Orientation orientation, int rol
return tr("Date and time the world was last played."); return tr("Date and time the world was last played.");
case SizeColumn: case SizeColumn:
return tr("Size of the world on disk."); return tr("Size of the world on disk.");
case InfoColumn:
return tr("Information and warnings about the world.");
default: default:
return QVariant(); return QVariant();
} }

View File

@ -33,7 +33,8 @@ public:
NameColumn, NameColumn,
GameModeColumn, GameModeColumn,
LastPlayedColumn, LastPlayedColumn,
SizeColumn SizeColumn,
InfoColumn
}; };
enum Roles enum Roles
@ -112,6 +113,8 @@ public:
return m_dir; return m_dir;
} }
QString instDirPath() const;
const QList<World> &allWorlds() const const QList<World> &allWorlds() const
{ {
return worlds; return worlds;

View File

@ -39,13 +39,17 @@
#include <FileSystem.h> #include <FileSystem.h>
#include <QDebug> #include <QDebug>
#include <QFileSystemWatcher> #include <QFileSystemWatcher>
#include <QIcon>
#include <QMimeData> #include <QMimeData>
#include <QString> #include <QString>
#include <QStyle>
#include <QThreadPool> #include <QThreadPool>
#include <QUrl> #include <QUrl>
#include <QUuid> #include <QUuid>
#include <algorithm> #include <algorithm>
#include "Application.h"
#include "minecraft/mod/tasks/LocalModParseTask.h" #include "minecraft/mod/tasks/LocalModParseTask.h"
#include "minecraft/mod/tasks/ModFolderLoadTask.h" #include "minecraft/mod/tasks/ModFolderLoadTask.h"
#include "modplatform/ModIndex.h" #include "modplatform/ModIndex.h"
@ -97,8 +101,24 @@ QVariant ModFolderModel::data(const QModelIndex &index, int role) const
} }
case Qt::ToolTipRole: case Qt::ToolTipRole:
if (column == NAME_COLUMN) {
if (at(row)->isSymLinkUnder(instDirPath())) {
return m_resources[row]->internal_id() +
tr("\nWarning: This resource is symbolicly linked from elsewhere. Editing it will also change the origonal") +
tr("\nCanonical Path: %1").arg(at(row)->fileinfo().canonicalFilePath());
}
if (at(row)->isMoreThanOneHardLink()) {
return m_resources[row]->internal_id() +
tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the origonal");
}
}
return m_resources[row]->internal_id(); return m_resources[row]->internal_id();
case Qt::DecorationRole: {
if (column == NAME_COLUMN && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink()))
return APPLICATION->getThemedIcon("status-yellow");
return {};
}
case Qt::CheckStateRole: case Qt::CheckStateRole:
switch (column) switch (column)
{ {

View File

@ -1,6 +1,8 @@
#include "Resource.h" #include "Resource.h"
#include <QRegularExpression> #include <QRegularExpression>
#include <QFileInfo>
#include "FileSystem.h" #include "FileSystem.h"
@ -152,3 +154,21 @@ bool Resource::destroy()
return FS::deletePath(m_file_info.filePath()); return FS::deletePath(m_file_info.filePath());
} }
bool Resource::isSymLinkUnder(const QString& instPath) const
{
if (isSymLink())
return true;
auto instDir = QDir(instPath);
auto relAbsPath = instDir.relativeFilePath(m_file_info.absoluteFilePath());
auto relCanonPath = instDir.relativeFilePath(m_file_info.canonicalFilePath());
return relAbsPath != relCanonPath;
}
bool Resource::isMoreThanOneHardLink() const
{
return FS::hardLinkCount(m_file_info.absoluteFilePath()) > 1;
}

View File

@ -94,6 +94,19 @@ class Resource : public QObject {
// Delete all files of this resource. // Delete all files of this resource.
bool destroy(); bool destroy();
[[nodiscard]] auto isSymLink() const -> bool { return m_file_info.isSymLink(); }
/**
* @brief Take a instance path, checks if the file pointed to by the resource is a symlink or under a symlink in that instance
*
* @param instPath path to an instance directory
* @return true
* @return false
*/
[[nodiscard]] bool isSymLinkUnder(const QString& instPath) const;
[[nodiscard]] bool isMoreThanOneHardLink() const;
protected: protected:
/* The file corresponding to this resource. */ /* The file corresponding to this resource. */
QFileInfo m_file_info; QFileInfo m_file_info;

View File

@ -2,10 +2,14 @@
#include <QCoreApplication> #include <QCoreApplication>
#include <QDebug> #include <QDebug>
#include <QFileInfo>
#include <QIcon>
#include <QMimeData> #include <QMimeData>
#include <QStyle>
#include <QThreadPool> #include <QThreadPool>
#include <QUrl> #include <QUrl>
#include "Application.h"
#include "FileSystem.h" #include "FileSystem.h"
#include "minecraft/mod/tasks/BasicFolderLoadTask.h" #include "minecraft/mod/tasks/BasicFolderLoadTask.h"
@ -417,7 +421,25 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const
return {}; return {};
} }
case Qt::ToolTipRole: case Qt::ToolTipRole:
if (column == NAME_COLUMN) {
if (at(row).isSymLinkUnder(instDirPath())) {
return m_resources[row]->internal_id() +
tr("\nWarning: This resource is symbolicly linked from elsewhere. Editing it will also change the origonal") +
tr("\nCanonical Path: %1").arg(at(row).fileinfo().canonicalFilePath());;
}
if (at(row).isMoreThanOneHardLink()) {
return m_resources[row]->internal_id() +
tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the origonal");
}
}
return m_resources[row]->internal_id(); return m_resources[row]->internal_id();
case Qt::DecorationRole: {
if (column == NAME_COLUMN && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink()))
return APPLICATION->getThemedIcon("status-yellow");
return {};
}
case Qt::CheckStateRole: case Qt::CheckStateRole:
switch (column) { switch (column) {
case ACTIVE_COLUMN: case ACTIVE_COLUMN:
@ -531,3 +553,7 @@ void ResourceFolderModel::enableInteraction(bool enabled)
return (compare_result.first < 0); return (compare_result.first < 0);
return (compare_result.first > 0); return (compare_result.first > 0);
} }
QString ResourceFolderModel::instDirPath() const {
return QFileInfo(m_dir.filePath("../..")).absoluteFilePath();
}

View File

@ -125,6 +125,8 @@ class ResourceFolderModel : public QAbstractListModel {
[[nodiscard]] bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override; [[nodiscard]] bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override;
}; };
QString instDirPath() const;
public slots: public slots:
void enableInteraction(bool enabled); void enableInteraction(bool enabled);
void disableInteraction(bool disabled) { enableInteraction(!disabled); } void disableInteraction(bool disabled) { enableInteraction(!disabled); }

View File

@ -36,6 +36,10 @@
#include "ResourcePackFolderModel.h" #include "ResourcePackFolderModel.h"
#include <QIcon>
#include <QStyle>
#include "Application.h"
#include "Version.h" #include "Version.h"
#include "minecraft/mod/tasks/BasicFolderLoadTask.h" #include "minecraft/mod/tasks/BasicFolderLoadTask.h"
@ -78,12 +82,28 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const
default: default:
return {}; return {};
} }
case Qt::DecorationRole: {
if (column == NAME_COLUMN && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink()))
return APPLICATION->getThemedIcon("status-yellow");
return {};
}
case Qt::ToolTipRole: { case Qt::ToolTipRole: {
if (column == PackFormatColumn) { if (column == PackFormatColumn) {
//: The string being explained by this is in the format: ID (Lower version - Upper version) //: The string being explained by this is in the format: ID (Lower version - Upper version)
return tr("The resource pack format ID, as well as the Minecraft versions it was designed for."); return tr("The resource pack format ID, as well as the Minecraft versions it was designed for.");
} }
if (column == NAME_COLUMN) {
if (at(row)->isSymLinkUnder(instDirPath())) {
return m_resources[row]->internal_id() +
tr("\nWarning: This resource is symbolicly linked from elsewhere. Editing it will also change the origonal") +
tr("\nCanonical Path: %1").arg(at(row)->fileinfo().canonicalFilePath());;
}
if (at(row)->isMoreThanOneHardLink()) {
return m_resources[row]->internal_id() +
tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the origonal");
}
}
return m_resources[row]->internal_id(); return m_resources[row]->internal_id();
} }
case Qt::CheckStateRole: case Qt::CheckStateRole:

View File

@ -107,6 +107,7 @@ WorldListPage::WorldListPage(BaseInstance *inst, std::shared_ptr<WorldList> worl
auto head = ui->worldTreeView->header(); auto head = ui->worldTreeView->header();
head->setSectionResizeMode(0, QHeaderView::Stretch); head->setSectionResizeMode(0, QHeaderView::Stretch);
head->setSectionResizeMode(1, QHeaderView::ResizeToContents); head->setSectionResizeMode(1, QHeaderView::ResizeToContents);
head->setSectionResizeMode(4, QHeaderView::ResizeToContents);
connect(ui->worldTreeView->selectionModel(), &QItemSelectionModel::currentChanged, this, &WorldListPage::worldChanged); connect(ui->worldTreeView->selectionModel(), &QItemSelectionModel::currentChanged, this, &WorldListPage::worldChanged);
worldChanged(QModelIndex(), QModelIndex()); worldChanged(QModelIndex(), QModelIndex());