new builds menu and modloaders implementation
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
#include "buildarchiveworker.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
|
||||
#include <QtCore/private/qzipreader_p.h>
|
||||
#include <QtCore/private/qzipwriter_p.h>
|
||||
|
||||
namespace {
|
||||
|
||||
// Имена внутри архива приходят из файла и превращаются в пути на диске —
|
||||
// «../..» увёл бы распаковку за пределы .minecraft. Правило то же, что у
|
||||
// установщика версий.
|
||||
bool isSafeRelativePath(const QString &path)
|
||||
{
|
||||
if (path.isEmpty() || path.startsWith(u'/') || path.contains(u'\\') || path.contains(u':'))
|
||||
return false;
|
||||
const QStringList parts = path.split(u'/');
|
||||
for (const QString &part : parts) {
|
||||
if (part == QLatin1String(".") || part == QLatin1String(".."))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Файлы, которые попадут в архив, — относительными путями от gameDir.
|
||||
QStringList collectFiles(const QString &gameDir, const QStringList &excludeTop)
|
||||
{
|
||||
QStringList result;
|
||||
const QDir root(gameDir);
|
||||
const QFileInfoList top = root.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot
|
||||
| QDir::Hidden | QDir::System);
|
||||
for (const QFileInfo &entry : top) {
|
||||
if (excludeTop.contains(entry.fileName()))
|
||||
continue;
|
||||
if (entry.isSymLink())
|
||||
continue; // ссылку в архив не положить, а идти по ней опасно
|
||||
if (entry.isFile()) {
|
||||
result << entry.fileName();
|
||||
continue;
|
||||
}
|
||||
if (!entry.isDir())
|
||||
continue;
|
||||
|
||||
QDirIterator it(entry.absoluteFilePath(),
|
||||
QDir::Files | QDir::Hidden | QDir::System | QDir::NoSymLinks,
|
||||
QDirIterator::Subdirectories);
|
||||
while (it.hasNext())
|
||||
result << root.relativeFilePath(it.next());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
BuildArchiveWorker::BuildArchiveWorker(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void BuildArchiveWorker::archive(const QString &gameDir,
|
||||
const QString &tempZipPath,
|
||||
const QStringList &excludeTop)
|
||||
{
|
||||
const QDir root(gameDir);
|
||||
if (!root.exists()) {
|
||||
emit finished(false, tr("Папка игры не найдена: %1").arg(QDir::toNativeSeparators(gameDir)));
|
||||
return;
|
||||
}
|
||||
|
||||
const QStringList files = collectFiles(gameDir, excludeTop);
|
||||
emit progress(0, files.size(), {});
|
||||
|
||||
{
|
||||
QZipWriter writer(tempZipPath);
|
||||
if (writer.status() != QZipWriter::NoError) {
|
||||
emit finished(false,
|
||||
tr("Не удалось создать архив %1")
|
||||
.arg(QDir::toNativeSeparators(tempZipPath)));
|
||||
return;
|
||||
}
|
||||
// Миры и конфиги сжимаются хорошо, уже сжатые jar-моды — нет: пусть
|
||||
// решает эвристика, а не мы.
|
||||
writer.setCompressionPolicy(QZipWriter::AutoCompress);
|
||||
|
||||
int done = 0;
|
||||
for (const QString &relative : files) {
|
||||
if (canceled()) {
|
||||
writer.close();
|
||||
QFile::remove(tempZipPath);
|
||||
emit finished(false, tr("Архивация отменена"));
|
||||
return;
|
||||
}
|
||||
|
||||
QFile file(root.absoluteFilePath(relative));
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
// Один нечитаемый файл не повод потерять всю сборку, но и молча
|
||||
// выкидывать его нельзя — иначе пользователь узнает о пропаже
|
||||
// уже после очистки .minecraft.
|
||||
writer.close();
|
||||
QFile::remove(tempZipPath);
|
||||
emit finished(false,
|
||||
tr("Не удалось прочитать %1: %2")
|
||||
.arg(QDir::toNativeSeparators(relative), file.errorString()));
|
||||
return;
|
||||
}
|
||||
writer.addFile(relative, &file);
|
||||
file.close();
|
||||
|
||||
if (writer.status() != QZipWriter::NoError) {
|
||||
writer.close();
|
||||
QFile::remove(tempZipPath);
|
||||
emit finished(false,
|
||||
tr("Ошибка записи архива на файле %1")
|
||||
.arg(QDir::toNativeSeparators(relative)));
|
||||
return;
|
||||
}
|
||||
emit progress(++done, files.size(), relative);
|
||||
}
|
||||
writer.close();
|
||||
}
|
||||
|
||||
emit finished(true, {});
|
||||
}
|
||||
|
||||
void BuildArchiveWorker::clear(const QString &gameDir, const QStringList &keepTop)
|
||||
{
|
||||
QDir root(gameDir);
|
||||
if (!root.exists()) {
|
||||
emit finished(true, {}); // чистить нечего
|
||||
return;
|
||||
}
|
||||
|
||||
QFileInfoList victims;
|
||||
const QFileInfoList top = root.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot
|
||||
| QDir::Hidden | QDir::System);
|
||||
for (const QFileInfo &entry : top) {
|
||||
if (!keepTop.contains(entry.fileName()))
|
||||
victims << entry;
|
||||
}
|
||||
|
||||
emit progress(0, victims.size(), {});
|
||||
int done = 0;
|
||||
for (const QFileInfo &entry : victims) {
|
||||
// Отмена посреди очистки оставила бы полупустую папку, которую нечем
|
||||
// склеить обратно, — на этом шаге её не слушаем.
|
||||
const bool ok = entry.isDir() && !entry.isSymLink()
|
||||
? QDir(entry.absoluteFilePath()).removeRecursively()
|
||||
: QFile::remove(entry.absoluteFilePath());
|
||||
if (!ok) {
|
||||
emit finished(false,
|
||||
tr("Не удалось удалить %1")
|
||||
.arg(QDir::toNativeSeparators(entry.absoluteFilePath())));
|
||||
return;
|
||||
}
|
||||
emit progress(++done, victims.size(), entry.fileName());
|
||||
}
|
||||
|
||||
emit finished(true, {});
|
||||
}
|
||||
|
||||
void BuildArchiveWorker::restore(const QString &zipPath, const QString &gameDir)
|
||||
{
|
||||
QZipReader reader(zipPath);
|
||||
if (!reader.isReadable()) {
|
||||
emit finished(false,
|
||||
tr("Не удалось открыть архив %1").arg(QDir::toNativeSeparators(zipPath)));
|
||||
return;
|
||||
}
|
||||
|
||||
const QList<QZipReader::FileInfo> entries = reader.fileInfoList();
|
||||
emit progress(0, entries.size(), {});
|
||||
|
||||
const QDir root(gameDir);
|
||||
if (!QDir().mkpath(gameDir)) {
|
||||
emit finished(false,
|
||||
tr("Не удалось создать %1").arg(QDir::toNativeSeparators(gameDir)));
|
||||
return;
|
||||
}
|
||||
|
||||
int done = 0;
|
||||
for (const QZipReader::FileInfo &entry : entries) {
|
||||
if (canceled()) {
|
||||
emit finished(false, tr("Распаковка отменена"));
|
||||
return;
|
||||
}
|
||||
if (!entry.isFile) {
|
||||
emit progress(++done, entries.size(), entry.filePath);
|
||||
continue;
|
||||
}
|
||||
if (!isSafeRelativePath(entry.filePath)) {
|
||||
emit finished(false,
|
||||
tr("Архив повреждён: недопустимый путь «%1»").arg(entry.filePath));
|
||||
return;
|
||||
}
|
||||
|
||||
const QString target = root.absoluteFilePath(entry.filePath);
|
||||
if (!QDir().mkpath(QFileInfo(target).absolutePath())) {
|
||||
emit finished(false,
|
||||
tr("Не удалось создать %1")
|
||||
.arg(QDir::toNativeSeparators(QFileInfo(target).absolutePath())));
|
||||
return;
|
||||
}
|
||||
|
||||
QFile file(target);
|
||||
const QByteArray data = reader.fileData(entry.filePath);
|
||||
if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size()) {
|
||||
emit finished(false,
|
||||
tr("Не удалось записать %1: %2")
|
||||
.arg(QDir::toNativeSeparators(entry.filePath), file.errorString()));
|
||||
return;
|
||||
}
|
||||
file.close();
|
||||
emit progress(++done, entries.size(), entry.filePath);
|
||||
}
|
||||
|
||||
emit finished(true, {});
|
||||
}
|
||||
Reference in New Issue
Block a user