mirror of
https://github.com/PrismLauncher/PrismLauncher.git
synced 2025-09-16 15:35:52 -04:00
move ExtractZipTask
Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
This commit is contained in:
parent
270ff012ae
commit
43ceacbe56
@ -32,8 +32,8 @@ set(CORE_SOURCES
|
||||
archive/ArchiveWriter.h
|
||||
archive/ExportToZipTask.cpp
|
||||
archive/ExportToZipTask.h
|
||||
Untar.h
|
||||
Untar.cpp
|
||||
archive/ExtractZipTask.cpp
|
||||
archive/ExtractZipTask.h
|
||||
StringUtils.h
|
||||
StringUtils.cpp
|
||||
QVariantUtils.h
|
||||
|
@ -1701,4 +1701,14 @@ QString getUniqueResourceName(const QString& filePath)
|
||||
|
||||
return newFileName;
|
||||
}
|
||||
bool removeFiles(QStringList listFile)
|
||||
{
|
||||
bool ret = true;
|
||||
// For each file
|
||||
for (int i = 0; i < listFile.count(); i++) {
|
||||
// Remove
|
||||
ret = ret && QFile::remove(listFile.at(i));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
} // namespace FS
|
||||
|
@ -291,6 +291,8 @@ bool move(const QString& source, const QString& dest);
|
||||
*/
|
||||
bool deletePath(QString path);
|
||||
|
||||
bool removeFiles(QStringList listFile);
|
||||
|
||||
/**
|
||||
* Trash a folder / file
|
||||
*/
|
||||
|
@ -38,10 +38,11 @@
|
||||
|
||||
#include "Application.h"
|
||||
#include "FileSystem.h"
|
||||
#include "MMCZip.h"
|
||||
#include "NullInstance.h"
|
||||
|
||||
#include "QObjectPtr.h"
|
||||
#include "archive/ArchiveReader.h"
|
||||
#include "archive/ExtractZipTask.h"
|
||||
#include "icons/IconList.h"
|
||||
#include "icons/IconUtils.h"
|
||||
|
||||
@ -54,8 +55,8 @@
|
||||
|
||||
#include "net/ApiDownload.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QtConcurrentRun>
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
|
||||
#include <quazip/quazipdir.h>
|
||||
@ -109,38 +110,26 @@ void InstanceImportTask::downloadFromUrl()
|
||||
filesNetJob->start();
|
||||
}
|
||||
|
||||
QString InstanceImportTask::getRootFromZip(QuaZip* zip, const QString& root)
|
||||
QString InstanceImportTask::getRootFromZip(QStringList files)
|
||||
{
|
||||
if (!isRunning()) {
|
||||
return {};
|
||||
}
|
||||
QuaZipDir rootDir(zip, root);
|
||||
for (auto&& fileName : rootDir.entryList(QDir::Files)) {
|
||||
for (auto&& fileName : files) {
|
||||
setDetails(fileName);
|
||||
if (fileName == "instance.cfg") {
|
||||
QFileInfo fileInfo(fileName);
|
||||
if (fileInfo.fileName() == "instance.cfg") {
|
||||
qDebug() << "MultiMC:" << true;
|
||||
m_modpackType = ModpackType::MultiMC;
|
||||
return root;
|
||||
return fileInfo.path();
|
||||
}
|
||||
if (fileName == "manifest.json") {
|
||||
if (fileInfo.fileName() == "manifest.json") {
|
||||
qDebug() << "Flame:" << true;
|
||||
m_modpackType = ModpackType::Flame;
|
||||
return root;
|
||||
return fileInfo.path();
|
||||
}
|
||||
|
||||
QCoreApplication::processEvents();
|
||||
}
|
||||
|
||||
// Recurse the search to non-ignored subfolders
|
||||
for (auto&& fileName : rootDir.entryList(QDir::Dirs)) {
|
||||
if ("overrides/" == fileName)
|
||||
continue;
|
||||
|
||||
QString result = getRootFromZip(zip, root + fileName);
|
||||
if (!result.isEmpty())
|
||||
return result;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
@ -151,13 +140,12 @@ void InstanceImportTask::processZipPack()
|
||||
qDebug() << "Attempting to create instance from" << m_archivePath;
|
||||
|
||||
// open the zip and find relevant files in it
|
||||
auto packZip = std::make_shared<QuaZip>(m_archivePath);
|
||||
if (!packZip->open(QuaZip::mdUnzip)) {
|
||||
MMCZip::ArchiveReader packZip(m_archivePath);
|
||||
if (!packZip.collectFiles()) {
|
||||
emitFailed(tr("Unable to open supplied modpack zip file."));
|
||||
return;
|
||||
}
|
||||
|
||||
QuaZipDir packZipDir(packZip.get());
|
||||
qDebug() << "Attempting to determine instance type";
|
||||
|
||||
QString root;
|
||||
@ -165,18 +153,18 @@ void InstanceImportTask::processZipPack()
|
||||
// NOTE: Prioritize modpack platforms that aren't searched for recursively.
|
||||
// Especially Flame has a very common filename for its manifest, which may appear inside overrides for example
|
||||
// https://docs.modrinth.com/docs/modpacks/format_definition/#storage
|
||||
if (packZipDir.exists("/modrinth.index.json")) {
|
||||
if (packZip.exists("/modrinth.index.json")) {
|
||||
// process as Modrinth pack
|
||||
qDebug() << "Modrinth:" << true;
|
||||
m_modpackType = ModpackType::Modrinth;
|
||||
} else if (packZipDir.exists("/bin/modpack.jar") || packZipDir.exists("/bin/version.json")) {
|
||||
} else if (packZip.exists("/bin/modpack.jar") || packZip.exists("/bin/version.json")) {
|
||||
// process as Technic pack
|
||||
qDebug() << "Technic:" << true;
|
||||
extractDir.mkpath("minecraft");
|
||||
extractDir.cd("minecraft");
|
||||
m_modpackType = ModpackType::Technic;
|
||||
} else {
|
||||
root = getRootFromZip(packZip.get());
|
||||
root = getRootFromZip(packZip.getFiles());
|
||||
setDetails("");
|
||||
}
|
||||
if (m_modpackType == ModpackType::Unknown) {
|
||||
@ -186,7 +174,7 @@ void InstanceImportTask::processZipPack()
|
||||
setStatus(tr("Extracting modpack"));
|
||||
|
||||
// make sure we extract just the pack
|
||||
auto zipTask = makeShared<MMCZip::ExtractZipTask>(packZip, extractDir, root);
|
||||
auto zipTask = makeShared<MMCZip::ExtractZipTask>(m_archivePath, extractDir, root);
|
||||
|
||||
auto progressStep = std::make_shared<TaskStepProgress>();
|
||||
connect(zipTask.get(), &Task::finished, this, [this, progressStep] {
|
||||
|
@ -58,7 +58,7 @@ class InstanceImportTask : public InstanceTask {
|
||||
void processTechnic();
|
||||
void processFlame();
|
||||
void processModrinth();
|
||||
QString getRootFromZip(QuaZip* zip, const QString& root = "");
|
||||
QString getRootFromZip(QStringList files);
|
||||
|
||||
private slots:
|
||||
void processZipPack();
|
||||
|
@ -265,7 +265,7 @@ std::optional<QStringList> extractSubDir(ArchiveReader* zip, const QString& subd
|
||||
return true;
|
||||
})) {
|
||||
qWarning() << "Failed to parse file" << zip->getZipName();
|
||||
JlCompress::removeFile(extracted);
|
||||
FS::removeFiles(extracted);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
@ -363,140 +363,4 @@ bool collectFileListRecursively(const QString& rootDir, const QString& subDir, Q
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#if defined(LAUNCHER_APPLICATION)
|
||||
|
||||
void ExtractZipTask::executeTask()
|
||||
{
|
||||
if (!m_input->isOpen() && !m_input->open(QuaZip::mdUnzip)) {
|
||||
emitFailed(tr("Unable to open supplied zip file."));
|
||||
return;
|
||||
}
|
||||
m_zip_future = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return extractZip(); });
|
||||
connect(&m_zip_watcher, &QFutureWatcher<ZipResult>::finished, this, &ExtractZipTask::finish);
|
||||
m_zip_watcher.setFuture(m_zip_future);
|
||||
}
|
||||
|
||||
auto ExtractZipTask::extractZip() -> ZipResult
|
||||
{
|
||||
auto target = m_output_dir.absolutePath();
|
||||
auto target_top_dir = QUrl::fromLocalFile(target);
|
||||
|
||||
QStringList extracted;
|
||||
|
||||
qDebug() << "Extracting subdir" << m_subdirectory << "from" << m_input->getZipName() << "to" << target;
|
||||
auto numEntries = m_input->getEntriesCount();
|
||||
if (numEntries < 0) {
|
||||
return ZipResult(tr("Failed to enumerate files in archive"));
|
||||
}
|
||||
if (numEntries == 0) {
|
||||
logWarning(tr("Extracting empty archives seems odd..."));
|
||||
return ZipResult();
|
||||
}
|
||||
if (!m_input->goToFirstFile()) {
|
||||
return ZipResult(tr("Failed to seek to first file in zip"));
|
||||
}
|
||||
|
||||
setStatus("Extracting files...");
|
||||
setProgress(0, numEntries);
|
||||
do {
|
||||
if (m_zip_future.isCanceled())
|
||||
return ZipResult();
|
||||
setProgress(m_progress + 1, m_progressTotal);
|
||||
QString file_name = m_input->getCurrentFileName();
|
||||
if (!file_name.startsWith(m_subdirectory))
|
||||
continue;
|
||||
|
||||
auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(m_subdirectory.size()));
|
||||
auto original_name = relative_file_name;
|
||||
setStatus("Unpacking: " + relative_file_name);
|
||||
|
||||
// Fix subdirs/files ending with a / getting transformed into absolute paths
|
||||
if (relative_file_name.startsWith('/'))
|
||||
relative_file_name = relative_file_name.mid(1);
|
||||
|
||||
// Fix weird "folders with a single file get squashed" thing
|
||||
QString sub_path;
|
||||
if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) {
|
||||
sub_path = relative_file_name.section('/', 0, -2) + '/';
|
||||
FS::ensureFolderPathExists(FS::PathCombine(target, sub_path));
|
||||
|
||||
relative_file_name = relative_file_name.split('/').last();
|
||||
}
|
||||
|
||||
QString target_file_path;
|
||||
if (relative_file_name.isEmpty()) {
|
||||
target_file_path = target + '/';
|
||||
} else {
|
||||
target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name);
|
||||
if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/'))
|
||||
target_file_path += '/';
|
||||
}
|
||||
|
||||
if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) {
|
||||
return ZipResult(tr("Extracting %1 was cancelled, because it was effectively outside of the target path %2")
|
||||
.arg(relative_file_name, target));
|
||||
}
|
||||
|
||||
if (!JlCompress::extractFile(m_input.get(), "", target_file_path)) {
|
||||
JlCompress::removeFile(extracted);
|
||||
return ZipResult(tr("Failed to extract file %1 to %2").arg(original_name, target_file_path));
|
||||
}
|
||||
|
||||
extracted.append(target_file_path);
|
||||
auto fileInfo = QFileInfo(target_file_path);
|
||||
if (fileInfo.isFile()) {
|
||||
auto permissions = fileInfo.permissions();
|
||||
auto maxPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser |
|
||||
QFileDevice::Permission::ReadGroup | QFileDevice::Permission::ReadOther;
|
||||
auto minPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser;
|
||||
|
||||
auto newPermisions = (permissions & maxPermisions) | minPermisions;
|
||||
if (newPermisions != permissions) {
|
||||
if (!QFile::setPermissions(target_file_path, newPermisions)) {
|
||||
logWarning(tr("Could not fix permissions for %1").arg(target_file_path));
|
||||
}
|
||||
}
|
||||
} else if (fileInfo.isDir()) {
|
||||
// Ensure the folder has the minimal required permissions
|
||||
QFile::Permissions minimalPermissions = QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner | QFile::ReadGroup |
|
||||
QFile::ExeGroup | QFile::ReadOther | QFile::ExeOther;
|
||||
|
||||
QFile::Permissions currentPermissions = fileInfo.permissions();
|
||||
if ((currentPermissions & minimalPermissions) != minimalPermissions) {
|
||||
if (!QFile::setPermissions(target_file_path, minimalPermissions)) {
|
||||
logWarning(tr("Could not fix permissions for %1").arg(target_file_path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path;
|
||||
} while (m_input->goToNextFile());
|
||||
|
||||
return ZipResult();
|
||||
}
|
||||
|
||||
void ExtractZipTask::finish()
|
||||
{
|
||||
if (m_zip_future.isCanceled()) {
|
||||
emitAborted();
|
||||
} else if (auto result = m_zip_future.result(); result.has_value()) {
|
||||
emitFailed(result.value());
|
||||
} else {
|
||||
emitSucceeded();
|
||||
}
|
||||
}
|
||||
|
||||
bool ExtractZipTask::abort()
|
||||
{
|
||||
if (m_zip_future.isRunning()) {
|
||||
m_zip_future.cancel();
|
||||
// NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not occur
|
||||
// immediately.
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
} // namespace MMCZip
|
||||
|
@ -108,36 +108,4 @@ bool extractFile(QString fileCompressed, QString file, QString dir);
|
||||
* \return true for success or false for failure
|
||||
*/
|
||||
bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFileFunction excludeFilter);
|
||||
|
||||
#if defined(LAUNCHER_APPLICATION)
|
||||
|
||||
class ExtractZipTask : public Task {
|
||||
Q_OBJECT
|
||||
public:
|
||||
ExtractZipTask(QString input, QDir outputDir, QString subdirectory = "")
|
||||
: ExtractZipTask(std::make_shared<QuaZip>(input), outputDir, subdirectory)
|
||||
{}
|
||||
ExtractZipTask(std::shared_ptr<QuaZip> input, QDir outputDir, QString subdirectory = "")
|
||||
: m_input(input), m_output_dir(outputDir), m_subdirectory(subdirectory)
|
||||
{}
|
||||
virtual ~ExtractZipTask() = default;
|
||||
|
||||
using ZipResult = std::optional<QString>;
|
||||
|
||||
protected:
|
||||
virtual void executeTask() override;
|
||||
bool abort() override;
|
||||
|
||||
ZipResult extractZip();
|
||||
void finish();
|
||||
|
||||
private:
|
||||
std::shared_ptr<QuaZip> m_input;
|
||||
QDir m_output_dir;
|
||||
QString m_subdirectory;
|
||||
|
||||
QFuture<ZipResult> m_zip_future;
|
||||
QFutureWatcher<ZipResult> m_zip_watcher;
|
||||
};
|
||||
#endif
|
||||
} // namespace MMCZip
|
||||
|
@ -1,260 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (c) 2023-2024 Trial97 <alexandru.tripon97@gmail.com>
|
||||
*
|
||||
* 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, version 3.
|
||||
*
|
||||
* 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, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This file incorporates work covered by the following copyright and
|
||||
* permission notice:
|
||||
*
|
||||
* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
#include "Untar.h"
|
||||
#include <quagzipfile.h>
|
||||
#include <QByteArray>
|
||||
#include <QFileInfo>
|
||||
#include <QIODevice>
|
||||
#include <QString>
|
||||
#include "FileSystem.h"
|
||||
|
||||
// adaptation of the:
|
||||
// - https://github.com/madler/zlib/blob/develop/contrib/untgz/untgz.c
|
||||
// - https://en.wikipedia.org/wiki/Tar_(computing)
|
||||
// - https://github.com/euroelessar/cutereader/blob/master/karchive/src/ktar.cpp
|
||||
|
||||
#define BLOCKSIZE 512
|
||||
#define SHORTNAMESIZE 100
|
||||
|
||||
enum class TypeFlag : char {
|
||||
Regular = '0', // regular file
|
||||
ARegular = 0, // regular file
|
||||
Link = '1', // link
|
||||
Symlink = '2', // reserved
|
||||
Character = '3', // character special
|
||||
Block = '4', // block special
|
||||
Directory = '5', // directory
|
||||
FIFO = '6', // FIFO special
|
||||
Contiguous = '7', // reserved
|
||||
// Posix stuff
|
||||
GlobalPosixHeader = 'g',
|
||||
ExtendedPosixHeader = 'x',
|
||||
// 'A'– 'Z' Vendor specific extensions(POSIX .1 - 1988)
|
||||
// GNU
|
||||
GNULongLink = 'K', /* long link name */
|
||||
GNULongName = 'L', /* long file name */
|
||||
};
|
||||
|
||||
// struct Header { /* byte offset */
|
||||
// char name[100]; /* 0 */
|
||||
// char mode[8]; /* 100 */
|
||||
// char uid[8]; /* 108 */
|
||||
// char gid[8]; /* 116 */
|
||||
// char size[12]; /* 124 */
|
||||
// char mtime[12]; /* 136 */
|
||||
// char chksum[8]; /* 148 */
|
||||
// TypeFlag typeflag; /* 156 */
|
||||
// char linkname[100]; /* 157 */
|
||||
// char magic[6]; /* 257 */
|
||||
// char version[2]; /* 263 */
|
||||
// char uname[32]; /* 265 */
|
||||
// char gname[32]; /* 297 */
|
||||
// char devmajor[8]; /* 329 */
|
||||
// char devminor[8]; /* 337 */
|
||||
// char prefix[155]; /* 345 */
|
||||
// /* 500 */
|
||||
// };
|
||||
|
||||
bool readLonglink(QIODevice* in, qint64 size, QByteArray& longlink)
|
||||
{
|
||||
qint64 n = 0;
|
||||
size--; // ignore trailing null
|
||||
if (size < 0) {
|
||||
qCritical() << "The filename size is negative";
|
||||
return false;
|
||||
}
|
||||
longlink.resize(size + (BLOCKSIZE - size % BLOCKSIZE)); // make the size divisible by BLOCKSIZE
|
||||
for (qint64 offset = 0; offset < longlink.size(); offset += BLOCKSIZE) {
|
||||
n = in->read(longlink.data() + offset, BLOCKSIZE);
|
||||
if (n != BLOCKSIZE) {
|
||||
qCritical() << "The expected blocksize was not respected for the name";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
longlink.truncate(qstrlen(longlink.constData()));
|
||||
return true;
|
||||
}
|
||||
|
||||
int getOctal(char* buffer, int maxlenght, bool* ok)
|
||||
{
|
||||
return QByteArray(buffer, qstrnlen(buffer, maxlenght)).toInt(ok, 8);
|
||||
}
|
||||
|
||||
QString decodeName(char* name)
|
||||
{
|
||||
return QFile::decodeName(QByteArray(name, qstrnlen(name, 100)));
|
||||
}
|
||||
bool Tar::extract(QIODevice* in, QString dst)
|
||||
{
|
||||
char buffer[BLOCKSIZE];
|
||||
QString name, symlink, firstFolderName;
|
||||
bool doNotReset = false, ok;
|
||||
while (true) {
|
||||
auto n = in->read(buffer, BLOCKSIZE);
|
||||
if (n != BLOCKSIZE) { // allways expect complete blocks
|
||||
qCritical() << "The expected blocksize was not respected";
|
||||
return false;
|
||||
}
|
||||
if (buffer[0] == 0) { // end of archive
|
||||
return true;
|
||||
}
|
||||
int mode = getOctal(buffer + 100, 8, &ok) | QFile::ReadUser | QFile::WriteUser; // hack to ensure write and read permisions
|
||||
if (!ok) {
|
||||
qCritical() << "The file mode can't be read";
|
||||
return false;
|
||||
}
|
||||
// there are names that are exactly 100 bytes long
|
||||
// and neither longlink nor \0 terminated (bug:101472)
|
||||
|
||||
if (name.isEmpty()) {
|
||||
name = decodeName(buffer);
|
||||
if (!firstFolderName.isEmpty() && name.startsWith(firstFolderName)) {
|
||||
name = name.mid(firstFolderName.size());
|
||||
}
|
||||
}
|
||||
if (symlink.isEmpty())
|
||||
symlink = decodeName(buffer);
|
||||
qint64 size = getOctal(buffer + 124, 12, &ok);
|
||||
if (!ok) {
|
||||
qCritical() << "The file size can't be read";
|
||||
return false;
|
||||
}
|
||||
switch (TypeFlag(buffer[156])) {
|
||||
case TypeFlag::Regular:
|
||||
/* fallthrough */
|
||||
case TypeFlag::ARegular: {
|
||||
auto fileName = FS::PathCombine(dst, name);
|
||||
if (!FS::ensureFilePathExists(fileName)) {
|
||||
qCritical() << "Can't ensure the file path to exist: " << fileName;
|
||||
return false;
|
||||
}
|
||||
QFile out(fileName);
|
||||
if (!out.open(QFile::WriteOnly)) {
|
||||
qCritical() << "Can't open file:" << fileName;
|
||||
return false;
|
||||
}
|
||||
out.setPermissions(QFile::Permissions(mode));
|
||||
while (size > 0) {
|
||||
QByteArray tmp(BLOCKSIZE, 0);
|
||||
n = in->read(tmp.data(), BLOCKSIZE);
|
||||
if (n != BLOCKSIZE) {
|
||||
qCritical() << "The expected blocksize was not respected when reading file";
|
||||
return false;
|
||||
}
|
||||
tmp.truncate(qMin(qint64(BLOCKSIZE), size));
|
||||
out.write(tmp);
|
||||
size -= BLOCKSIZE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TypeFlag::Directory: {
|
||||
if (firstFolderName.isEmpty()) {
|
||||
firstFolderName = name;
|
||||
break;
|
||||
}
|
||||
auto folderPath = FS::PathCombine(dst, name);
|
||||
if (!FS::ensureFolderPathExists(folderPath)) {
|
||||
qCritical() << "Can't ensure that folder exists: " << folderPath;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TypeFlag::GNULongLink: {
|
||||
doNotReset = true;
|
||||
QByteArray longlink;
|
||||
if (readLonglink(in, size, longlink)) {
|
||||
symlink = QFile::decodeName(longlink.constData());
|
||||
} else {
|
||||
qCritical() << "Failed to read long link";
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TypeFlag::GNULongName: {
|
||||
doNotReset = true;
|
||||
QByteArray longlink;
|
||||
if (readLonglink(in, size, longlink)) {
|
||||
name = QFile::decodeName(longlink.constData());
|
||||
} else {
|
||||
qCritical() << "Failed to read long name";
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TypeFlag::Link:
|
||||
/* fallthrough */
|
||||
case TypeFlag::Symlink: {
|
||||
auto fileName = FS::PathCombine(dst, name);
|
||||
if (!FS::create_link(FS::PathCombine(QFileInfo(fileName).path(), symlink), fileName)()) { // do not use symlinks
|
||||
qCritical() << "Can't create link for:" << fileName << " to:" << FS::PathCombine(QFileInfo(fileName).path(), symlink);
|
||||
return false;
|
||||
}
|
||||
FS::ensureFilePathExists(fileName);
|
||||
QFile::setPermissions(fileName, QFile::Permissions(mode));
|
||||
break;
|
||||
}
|
||||
case TypeFlag::Character:
|
||||
/* fallthrough */
|
||||
case TypeFlag::Block:
|
||||
/* fallthrough */
|
||||
case TypeFlag::FIFO:
|
||||
/* fallthrough */
|
||||
case TypeFlag::Contiguous:
|
||||
/* fallthrough */
|
||||
case TypeFlag::GlobalPosixHeader:
|
||||
/* fallthrough */
|
||||
case TypeFlag::ExtendedPosixHeader:
|
||||
/* fallthrough */
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (!doNotReset) {
|
||||
name.truncate(0);
|
||||
symlink.truncate(0);
|
||||
}
|
||||
doNotReset = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GZTar::extract(QString src, QString dst)
|
||||
{
|
||||
QuaGzipFile a(src);
|
||||
if (!a.open(QIODevice::ReadOnly)) {
|
||||
qCritical() << "Can't open tar file:" << src;
|
||||
return false;
|
||||
}
|
||||
return Tar::extract(&a, dst);
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
/*
|
||||
* Prism Launcher - Minecraft Launcher
|
||||
* Copyright (c) 2023-2024 Trial97 <alexandru.tripon97@gmail.com>
|
||||
*
|
||||
* 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, version 3.
|
||||
*
|
||||
* 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, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This file incorporates work covered by the following copyright and
|
||||
* permission notice:
|
||||
*
|
||||
* Copyright 2013-2021 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
#pragma once
|
||||
#include <QIODevice>
|
||||
|
||||
// this is a hack used for the java downloader (feel free to remove it in favor of a library)
|
||||
// both extract functions will extract the first folder inside dest(disregarding the prefix)
|
||||
namespace Tar {
|
||||
bool extract(QIODevice* in, QString dst);
|
||||
}
|
||||
|
||||
namespace GZTar {
|
||||
bool extract(QString src, QString dst);
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
#include "ArchiveReader.h"
|
||||
#include <archive_entry.h>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
|
||||
namespace MMCZip {
|
||||
QStringList ArchiveReader::getFiles()
|
||||
{
|
||||
@ -167,4 +170,32 @@ QString ArchiveReader::getZipName()
|
||||
{
|
||||
return m_archivePath;
|
||||
}
|
||||
|
||||
bool ArchiveReader::exists(const QString& filePath) const
|
||||
{
|
||||
if (filePath == QLatin1String("/") || filePath.isEmpty())
|
||||
return true;
|
||||
// Normalize input path (remove trailing slash, if any)
|
||||
QString normalizedPath = QDir::cleanPath(filePath);
|
||||
if (normalizedPath.startsWith('/'))
|
||||
normalizedPath.remove(0, 1);
|
||||
if (normalizedPath == QLatin1String("."))
|
||||
return true;
|
||||
if (normalizedPath == QLatin1String(".."))
|
||||
return false; // root only
|
||||
|
||||
// Check for exact file match
|
||||
if (m_fileNames.contains(normalizedPath, Qt::CaseInsensitive))
|
||||
return true;
|
||||
|
||||
// Check for directory existence by seeing if any file starts with that path
|
||||
QString dirPath = normalizedPath + QLatin1Char('/');
|
||||
for (const QString& f : m_fileNames) {
|
||||
if (f.startsWith(dirPath, Qt::CaseInsensitive))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace MMCZip
|
||||
|
@ -18,6 +18,7 @@ class ArchiveReader {
|
||||
QStringList getFiles();
|
||||
QString getZipName();
|
||||
bool collectFiles(bool onlyFiles = true);
|
||||
bool exists(const QString& filePath) const;
|
||||
|
||||
class File {
|
||||
public:
|
||||
|
130
launcher/archive/ExtractZipTask.cpp
Normal file
130
launcher/archive/ExtractZipTask.cpp
Normal file
@ -0,0 +1,130 @@
|
||||
#include "ExtractZipTask.h"
|
||||
#include <QtConcurrent>
|
||||
#include "FileSystem.h"
|
||||
#include "archive/ArchiveReader.h"
|
||||
|
||||
namespace MMCZip {
|
||||
|
||||
void ExtractZipTask::executeTask()
|
||||
{
|
||||
m_zip_future = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return extractZip(); });
|
||||
connect(&m_zip_watcher, &QFutureWatcher<ZipResult>::finished, this, &ExtractZipTask::finish);
|
||||
m_zip_watcher.setFuture(m_zip_future);
|
||||
}
|
||||
|
||||
auto ExtractZipTask::extractZip() -> ZipResult
|
||||
{
|
||||
auto target = m_output_dir.absolutePath();
|
||||
auto target_top_dir = QUrl::fromLocalFile(target);
|
||||
|
||||
QStringList extracted;
|
||||
|
||||
qDebug() << "Extracting subdir" << m_subdirectory << "from" << m_input.getZipName() << "to" << target;
|
||||
if (!m_input.collectFiles()) {
|
||||
return ZipResult(tr("Failed to enumerate files in archive"));
|
||||
}
|
||||
if (m_input.getFiles().isEmpty()) {
|
||||
logWarning(tr("Extracting empty archives seems odd..."));
|
||||
return ZipResult();
|
||||
}
|
||||
|
||||
int flags;
|
||||
|
||||
/* Select which attributes we want to restore. */
|
||||
flags = ARCHIVE_EXTRACT_TIME;
|
||||
flags |= ARCHIVE_EXTRACT_PERM;
|
||||
flags |= ARCHIVE_EXTRACT_ACL;
|
||||
flags |= ARCHIVE_EXTRACT_FFLAGS;
|
||||
|
||||
std::unique_ptr<archive, void (*)(archive*)> extPtr(archive_write_disk_new(), [](archive* a) {
|
||||
archive_write_close(a);
|
||||
archive_write_free(a);
|
||||
});
|
||||
auto ext = extPtr.get();
|
||||
archive_write_disk_set_options(ext, flags);
|
||||
archive_write_disk_set_standard_lookup(ext);
|
||||
|
||||
setStatus("Extracting files...");
|
||||
setProgress(0, m_input.getFiles().count());
|
||||
ZipResult result;
|
||||
if (!m_input.parse([this, &result, &target, &target_top_dir, ext, &extracted](ArchiveReader::File* f) {
|
||||
if (m_zip_future.isCanceled())
|
||||
return false;
|
||||
setProgress(m_progress + 1, m_progressTotal);
|
||||
QString file_name = f->filename();
|
||||
if (!file_name.startsWith(m_subdirectory)) {
|
||||
f->skip();
|
||||
return true;
|
||||
}
|
||||
|
||||
auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(m_subdirectory.size()));
|
||||
auto original_name = relative_file_name;
|
||||
setStatus("Unpacking: " + relative_file_name);
|
||||
|
||||
// Fix subdirs/files ending with a / getting transformed into absolute paths
|
||||
if (relative_file_name.startsWith('/'))
|
||||
relative_file_name = relative_file_name.mid(1);
|
||||
|
||||
// Fix weird "folders with a single file get squashed" thing
|
||||
QString sub_path;
|
||||
if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) {
|
||||
sub_path = relative_file_name.section('/', 0, -2) + '/';
|
||||
FS::ensureFolderPathExists(FS::PathCombine(target, sub_path));
|
||||
|
||||
relative_file_name = relative_file_name.split('/').last();
|
||||
}
|
||||
|
||||
QString target_file_path;
|
||||
if (relative_file_name.isEmpty()) {
|
||||
target_file_path = target + '/';
|
||||
} else {
|
||||
target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name);
|
||||
if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/'))
|
||||
target_file_path += '/';
|
||||
}
|
||||
|
||||
if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) {
|
||||
result = ZipResult(tr("Extracting %1 was cancelled, because it was effectively outside of the target path %2")
|
||||
.arg(relative_file_name, target));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!f->writeFile(ext, target_file_path)) {
|
||||
result = ZipResult(tr("Failed to extract file %1 to %2").arg(original_name, target_file_path));
|
||||
return false;
|
||||
}
|
||||
extracted.append(target_file_path);
|
||||
|
||||
qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path;
|
||||
return true;
|
||||
})) {
|
||||
FS::removeFiles(extracted);
|
||||
return result.has_value() || m_zip_future.isCanceled() ? result
|
||||
: ZipResult(tr("Failed to parse file %1").arg(m_input.getZipName()));
|
||||
}
|
||||
return ZipResult();
|
||||
}
|
||||
|
||||
void ExtractZipTask::finish()
|
||||
{
|
||||
if (m_zip_future.isCanceled()) {
|
||||
emitAborted();
|
||||
} else if (auto result = m_zip_future.result(); result.has_value()) {
|
||||
emitFailed(result.value());
|
||||
} else {
|
||||
emitSucceeded();
|
||||
}
|
||||
}
|
||||
|
||||
bool ExtractZipTask::abort()
|
||||
{
|
||||
if (m_zip_future.isRunning()) {
|
||||
m_zip_future.cancel();
|
||||
// NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not occur
|
||||
// immediately.
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace MMCZip
|
36
launcher/archive/ExtractZipTask.h
Normal file
36
launcher/archive/ExtractZipTask.h
Normal file
@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <QDir>
|
||||
#include <QFuture>
|
||||
#include <QFutureWatcher>
|
||||
#include "archive/ArchiveReader.h"
|
||||
#include "tasks/Task.h"
|
||||
|
||||
namespace MMCZip {
|
||||
|
||||
class ExtractZipTask : public Task {
|
||||
Q_OBJECT
|
||||
public:
|
||||
ExtractZipTask(QString input, QDir outputDir, QString subdirectory = "")
|
||||
: m_input(input), m_output_dir(outputDir), m_subdirectory(subdirectory)
|
||||
{}
|
||||
virtual ~ExtractZipTask() = default;
|
||||
|
||||
using ZipResult = std::optional<QString>;
|
||||
|
||||
protected:
|
||||
virtual void executeTask() override;
|
||||
bool abort() override;
|
||||
|
||||
ZipResult extractZip();
|
||||
void finish();
|
||||
|
||||
private:
|
||||
ArchiveReader m_input;
|
||||
QDir m_output_dir;
|
||||
QString m_subdirectory;
|
||||
|
||||
QFuture<ZipResult> m_zip_future;
|
||||
QFutureWatcher<ZipResult> m_zip_watcher;
|
||||
};
|
||||
} // namespace MMCZip
|
@ -18,10 +18,10 @@
|
||||
#include "java/download/ArchiveDownloadTask.h"
|
||||
#include <quazip.h>
|
||||
#include <memory>
|
||||
#include "MMCZip.h"
|
||||
|
||||
#include "Application.h"
|
||||
#include "Untar.h"
|
||||
#include "archive/ArchiveReader.h"
|
||||
#include "archive/ExtractZipTask.h"
|
||||
#include "net/ChecksumValidator.h"
|
||||
#include "net/NetJob.h"
|
||||
#include "tasks/Task.h"
|
||||
@ -67,68 +67,45 @@ void ArchiveDownloadTask::executeTask()
|
||||
void ArchiveDownloadTask::extractJava(QString input)
|
||||
{
|
||||
setStatus(tr("Extracting Java"));
|
||||
if (input.endsWith("tar")) {
|
||||
setStatus(tr("Extracting Java (Progress is not reported for tar archives)"));
|
||||
QFile in(input);
|
||||
if (!in.open(QFile::ReadOnly)) {
|
||||
emitFailed(tr("Unable to open supplied tar file."));
|
||||
return;
|
||||
}
|
||||
if (!Tar::extract(&in, QDir(m_final_path).absolutePath())) {
|
||||
emitFailed(tr("Unable to extract supplied tar file."));
|
||||
return;
|
||||
}
|
||||
emitSucceeded();
|
||||
return;
|
||||
} else if (input.endsWith("tar.gz") || input.endsWith("taz") || input.endsWith("tgz")) {
|
||||
setStatus(tr("Extracting Java (Progress is not reported for tar archives)"));
|
||||
if (!GZTar::extract(input, QDir(m_final_path).absolutePath())) {
|
||||
emitFailed(tr("Unable to extract supplied tar file."));
|
||||
return;
|
||||
}
|
||||
emitSucceeded();
|
||||
return;
|
||||
} else if (input.endsWith("zip")) {
|
||||
auto zip = std::make_shared<QuaZip>(input);
|
||||
if (!zip->open(QuaZip::mdUnzip)) {
|
||||
emitFailed(tr("Unable to open supplied zip file."));
|
||||
return;
|
||||
}
|
||||
auto files = zip->getFileNameList();
|
||||
if (files.isEmpty()) {
|
||||
emitFailed(tr("No files were found in the supplied zip file."));
|
||||
return;
|
||||
}
|
||||
m_task = makeShared<MMCZip::ExtractZipTask>(zip, m_final_path, files[0]);
|
||||
|
||||
auto progressStep = std::make_shared<TaskStepProgress>();
|
||||
connect(m_task.get(), &Task::finished, this, [this, progressStep] {
|
||||
progressStep->state = TaskStepState::Succeeded;
|
||||
stepProgress(*progressStep);
|
||||
});
|
||||
|
||||
connect(m_task.get(), &Task::succeeded, this, &ArchiveDownloadTask::emitSucceeded);
|
||||
connect(m_task.get(), &Task::aborted, this, &ArchiveDownloadTask::emitAborted);
|
||||
connect(m_task.get(), &Task::failed, this, [this, progressStep](QString reason) {
|
||||
progressStep->state = TaskStepState::Failed;
|
||||
stepProgress(*progressStep);
|
||||
emitFailed(reason);
|
||||
});
|
||||
connect(m_task.get(), &Task::stepProgress, this, &ArchiveDownloadTask::propagateStepProgress);
|
||||
|
||||
connect(m_task.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) {
|
||||
progressStep->update(current, total);
|
||||
stepProgress(*progressStep);
|
||||
});
|
||||
connect(m_task.get(), &Task::status, this, [this, progressStep](QString status) {
|
||||
progressStep->status = status;
|
||||
stepProgress(*progressStep);
|
||||
});
|
||||
m_task->start();
|
||||
MMCZip::ArchiveReader zip(input);
|
||||
if (!zip.collectFiles()) {
|
||||
emitFailed(tr("Unable to open supplied zip file."));
|
||||
return;
|
||||
}
|
||||
auto files = zip.getFiles();
|
||||
if (files.isEmpty()) {
|
||||
emitFailed(tr("No files were found in the supplied zip file."));
|
||||
return;
|
||||
}
|
||||
auto firstFolderParts = files[0].split('/', Qt::SkipEmptyParts);
|
||||
m_task = makeShared<MMCZip::ExtractZipTask>(input, m_final_path, firstFolderParts.value(0));
|
||||
|
||||
emitFailed(tr("Could not determine archive type!"));
|
||||
auto progressStep = std::make_shared<TaskStepProgress>();
|
||||
connect(m_task.get(), &Task::finished, this, [this, progressStep] {
|
||||
progressStep->state = TaskStepState::Succeeded;
|
||||
stepProgress(*progressStep);
|
||||
});
|
||||
|
||||
connect(m_task.get(), &Task::succeeded, this, &ArchiveDownloadTask::emitSucceeded);
|
||||
connect(m_task.get(), &Task::aborted, this, &ArchiveDownloadTask::emitAborted);
|
||||
connect(m_task.get(), &Task::failed, this, [this, progressStep](QString reason) {
|
||||
progressStep->state = TaskStepState::Failed;
|
||||
stepProgress(*progressStep);
|
||||
emitFailed(reason);
|
||||
});
|
||||
connect(m_task.get(), &Task::stepProgress, this, &ArchiveDownloadTask::propagateStepProgress);
|
||||
|
||||
connect(m_task.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) {
|
||||
progressStep->update(current, total);
|
||||
stepProgress(*progressStep);
|
||||
});
|
||||
connect(m_task.get(), &Task::status, this, [this, progressStep](QString status) {
|
||||
progressStep->status = status;
|
||||
stepProgress(*progressStep);
|
||||
});
|
||||
m_task->start();
|
||||
return;
|
||||
}
|
||||
|
||||
bool ArchiveDownloadTask::abort()
|
||||
|
@ -675,13 +675,6 @@ void PackInstallTask::extractConfigs()
|
||||
setStatus(tr("Extracting configs..."));
|
||||
|
||||
QDir extractDir(m_stagingPath);
|
||||
|
||||
QuaZip packZip(archivePath);
|
||||
if (!packZip.open(QuaZip::mdUnzip)) {
|
||||
emitFailed(tr("Failed to open pack configs %1!").arg(archivePath));
|
||||
return;
|
||||
}
|
||||
|
||||
m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload<QString, QString>::of(MMCZip::extractDir), archivePath,
|
||||
extractDir.absolutePath() + "/minecraft");
|
||||
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, [this]() { downloadMods(); });
|
||||
|
@ -102,12 +102,6 @@ void PackInstallTask::unzip()
|
||||
|
||||
QDir extractDir(m_stagingPath);
|
||||
|
||||
m_packZip.reset(new QuaZip(archivePath));
|
||||
if (!m_packZip->open(QuaZip::mdUnzip)) {
|
||||
emitFailed(tr("Failed to open modpack file %1!").arg(archivePath));
|
||||
return;
|
||||
}
|
||||
|
||||
m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload<QString, QString>::of(MMCZip::extractDir), archivePath,
|
||||
extractDir.absolutePath() + "/unzip");
|
||||
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &PackInstallTask::onUnzipFinished);
|
||||
|
@ -41,7 +41,6 @@ class PackInstallTask : public InstanceTask {
|
||||
private: /* data */
|
||||
shared_qobject_ptr<QNetworkAccessManager> m_network;
|
||||
bool abortable = false;
|
||||
std::unique_ptr<QuaZip> m_packZip;
|
||||
QFuture<std::optional<QStringList>> m_extractFuture;
|
||||
QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;
|
||||
NetJob::Ptr netJobContainer;
|
||||
|
Loading…
x
Reference in New Issue
Block a user