feat(filemanager): add archive compression and extraction
parent
6a6ca03f27
commit
291b5bdf31
@ -0,0 +1,721 @@
|
||||
#include "archivejob.h"
|
||||
|
||||
#include <zip.h>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QFileInfoList>
|
||||
#include <QUuid>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#ifdef Q_OS_UNIX
|
||||
#include <sys/stat.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
QString zipError(zip_t *archive)
|
||||
{
|
||||
if (!archive)
|
||||
return QStringLiteral("Unknown ZIP error");
|
||||
|
||||
return QString::fromUtf8(zip_strerror(archive));
|
||||
}
|
||||
|
||||
QString zipOpenError(int errorCode)
|
||||
{
|
||||
zip_error_t error;
|
||||
zip_error_init_with_code(&error, errorCode);
|
||||
const QString message = QString::fromUtf8(zip_error_strerror(&error));
|
||||
zip_error_fini(&error);
|
||||
return message;
|
||||
}
|
||||
|
||||
QString temporaryPath(const QString &parentPath, const QString &prefix)
|
||||
{
|
||||
return QDir(parentPath).filePath(QStringLiteral(".%1-%2")
|
||||
.arg(prefix, QUuid::createUuid().toString(QUuid::WithoutBraces)));
|
||||
}
|
||||
|
||||
bool isDirectoryEntry(const QString &name)
|
||||
{
|
||||
return name.endsWith(QLatin1Char('/'));
|
||||
}
|
||||
|
||||
bool hasSymbolicLinkParent(const QString &rootPath, const QString &relativePath)
|
||||
{
|
||||
const QStringList components = relativePath.split(QLatin1Char('/'), Qt::SkipEmptyParts);
|
||||
QString currentPath = rootPath;
|
||||
|
||||
for (qsizetype index = 0; index + 1 < components.size(); ++index) {
|
||||
currentPath = QDir(currentPath).filePath(components.at(index));
|
||||
const QFileInfo info(currentPath);
|
||||
if (info.isSymLink())
|
||||
return true;
|
||||
|
||||
if (!info.exists())
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
QString topLevelArchiveName(const QString &archivePath)
|
||||
{
|
||||
const qsizetype separator = archivePath.indexOf(QLatin1Char('/'));
|
||||
return separator < 0 ? archivePath : archivePath.left(separator);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ArchiveJob::ArchiveJob(Operation operation,
|
||||
const QStringList &inputs,
|
||||
const QString &outputPath,
|
||||
QObject *parent)
|
||||
: QThread(parent)
|
||||
, m_operation(operation)
|
||||
, m_inputs(inputs)
|
||||
, m_outputPath(outputPath)
|
||||
, m_cancelRequested(false)
|
||||
{
|
||||
}
|
||||
|
||||
ArchiveJob::~ArchiveJob()
|
||||
{
|
||||
cancel();
|
||||
wait();
|
||||
}
|
||||
|
||||
void ArchiveJob::cancel()
|
||||
{
|
||||
m_cancelRequested.store(true);
|
||||
}
|
||||
|
||||
bool ArchiveJob::isCanceled() const
|
||||
{
|
||||
return m_cancelRequested.load();
|
||||
}
|
||||
|
||||
void ArchiveJob::run()
|
||||
{
|
||||
QString error;
|
||||
const bool success = m_operation == Compress ? compress(&error) : extract(&error);
|
||||
// Once the final rename has succeeded, the operation is committed. A
|
||||
// cancellation arriving in the tiny window before this function reads
|
||||
// the flag must not turn a completed operation into a false cancellation.
|
||||
const bool canceled = !success && isCanceled();
|
||||
|
||||
emit completed(success && !canceled, canceled, error,
|
||||
success && !canceled ? m_outputPath : QString());
|
||||
}
|
||||
|
||||
void ArchiveJob::emitProgress(quint64 processed, quint64 total)
|
||||
{
|
||||
if (total == 0) {
|
||||
emit progressChanged(processed == 0 ? 0 : 100);
|
||||
return;
|
||||
}
|
||||
|
||||
const double ratio = static_cast<double>(processed) / static_cast<double>(total);
|
||||
emit progressChanged(qBound(0, static_cast<int>(ratio * 100.0), 100));
|
||||
}
|
||||
|
||||
bool ArchiveJob::collectCompressionEntries(const QString &sourcePath,
|
||||
const QString &archivePath,
|
||||
QList<CompressionEntry> *entries,
|
||||
QString *error) const
|
||||
{
|
||||
if (isCanceled())
|
||||
return false;
|
||||
|
||||
const QFileInfo info(sourcePath);
|
||||
if (!info.exists() && !info.isSymLink()) {
|
||||
*error = QStringLiteral("The source no longer exists: %1").arg(sourcePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (info.isSymLink()) {
|
||||
#ifdef Q_OS_UNIX
|
||||
const QString linkTarget = info.symLinkTarget();
|
||||
if (linkTarget.isEmpty()) {
|
||||
*error = QStringLiteral("Could not read symbolic link: %1").arg(sourcePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
entries->append({sourcePath, archivePath, false, 0, true, linkTarget});
|
||||
return true;
|
||||
#else
|
||||
*error = QStringLiteral("Symbolic links are not supported: %1").arg(sourcePath);
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (info.isDir()) {
|
||||
const QString directoryArchivePath = archivePath.endsWith(QLatin1Char('/'))
|
||||
? archivePath
|
||||
: archivePath + QLatin1Char('/');
|
||||
entries->append({sourcePath, directoryArchivePath, true, 0, false, QString()});
|
||||
|
||||
QDir directory(sourcePath);
|
||||
const QFileInfoList children = directory.entryInfoList(
|
||||
QDir::AllEntries | QDir::Hidden | QDir::System | QDir::NoDotAndDotDot,
|
||||
QDir::DirsFirst | QDir::Name);
|
||||
|
||||
for (const QFileInfo &child : children) {
|
||||
const QString childArchivePath = directoryArchivePath + child.fileName();
|
||||
if (!collectCompressionEntries(child.absoluteFilePath(), childArchivePath, entries, error))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!info.isFile()) {
|
||||
*error = QStringLiteral("The source is not a regular file: %1").arg(sourcePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
entries->append({sourcePath, archivePath, false, static_cast<quint64>(info.size()), false, QString()});
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ArchiveJob::compress(QString *error)
|
||||
{
|
||||
if (m_inputs.isEmpty()) {
|
||||
*error = QStringLiteral("No files were selected");
|
||||
return false;
|
||||
}
|
||||
|
||||
QList<CompressionEntry> entries;
|
||||
for (const QString &input : m_inputs) {
|
||||
const QFileInfo info(input);
|
||||
if (!collectCompressionEntries(input, info.fileName(), &entries, error))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isCanceled())
|
||||
return false;
|
||||
|
||||
const QString parentPath = QFileInfo(m_outputPath).absolutePath();
|
||||
if (!QDir().exists(parentPath)) {
|
||||
*error = QStringLiteral("The destination folder does not exist: %1").arg(parentPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString tempPath = temporaryPath(parentPath, QStringLiteral("cutefish-archive"));
|
||||
int openError = 0;
|
||||
const QByteArray tempPathBytes = QFile::encodeName(tempPath);
|
||||
zip_t *archive = zip_open(tempPathBytes.constData(), ZIP_CREATE | ZIP_TRUNCATE, &openError);
|
||||
if (!archive) {
|
||||
QFile::remove(tempPath);
|
||||
*error = zipOpenError(openError);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (zip_register_progress_callback_with_state(archive, 0.01, &ArchiveJob::zipProgressCallback,
|
||||
nullptr, this) != 0 ||
|
||||
zip_register_cancel_callback_with_state(archive, &ArchiveJob::zipCancelCallback,
|
||||
nullptr, this) != 0) {
|
||||
*error = zipError(archive);
|
||||
zip_discard(archive);
|
||||
QFile::remove(tempPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
emit progressChanged(0);
|
||||
for (const CompressionEntry &entry : entries) {
|
||||
if (isCanceled()) {
|
||||
zip_discard(archive);
|
||||
QFile::remove(tempPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Keep the progress UI focused on the selected item. A folder may
|
||||
// contain many entries, but its internal archive paths are not useful
|
||||
// to show while the archive is being created.
|
||||
emit currentFileChanged(topLevelArchiveName(entry.archivePath));
|
||||
|
||||
const QByteArray archiveName = entry.archivePath.toUtf8();
|
||||
if (entry.directory) {
|
||||
if (zip_dir_add(archive, archiveName.constData(), ZIP_FL_ENC_UTF_8) < 0) {
|
||||
*error = zipError(archive);
|
||||
zip_discard(archive);
|
||||
QFile::remove(tempPath);
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
zip_source_t *source = nullptr;
|
||||
if (entry.symbolicLink) {
|
||||
const QByteArray linkTarget = entry.linkTarget.toUtf8();
|
||||
const size_t bufferSize = qMax<size_t>(linkTarget.size(), 1);
|
||||
char *buffer = static_cast<char *>(std::malloc(bufferSize));
|
||||
if (!buffer) {
|
||||
*error = QStringLiteral("Could not allocate memory for symbolic link");
|
||||
zip_discard(archive);
|
||||
QFile::remove(tempPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::memcpy(buffer, linkTarget.constData(), static_cast<size_t>(linkTarget.size()));
|
||||
source = zip_source_buffer(archive, buffer, linkTarget.size(), 1);
|
||||
if (!source)
|
||||
std::free(buffer);
|
||||
} else {
|
||||
const QByteArray sourceName = QFile::encodeName(entry.sourcePath);
|
||||
source = zip_source_file(archive, sourceName.constData(), 0, -1);
|
||||
}
|
||||
|
||||
if (!source) {
|
||||
*error = zipError(archive);
|
||||
zip_discard(archive);
|
||||
QFile::remove(tempPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
const zip_int64_t entryIndex = zip_file_add(archive, archiveName.constData(), source,
|
||||
ZIP_FL_ENC_UTF_8);
|
||||
if (entryIndex < 0) {
|
||||
zip_source_free(source);
|
||||
*error = zipError(archive);
|
||||
zip_discard(archive);
|
||||
QFile::remove(tempPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use a fast, low-level DEFLATE profile. This keeps compression close
|
||||
// to Finder's quick archive behavior while avoiding an unnecessarily
|
||||
// expensive maximum-compression pass.
|
||||
if (zip_set_file_compression(archive,
|
||||
static_cast<zip_uint64_t>(entryIndex),
|
||||
ZIP_CM_DEFLATE,
|
||||
1) != 0) {
|
||||
*error = zipError(archive);
|
||||
zip_discard(archive);
|
||||
QFile::remove(tempPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (entry.symbolicLink &&
|
||||
zip_file_set_external_attributes(archive,
|
||||
static_cast<zip_uint64_t>(entryIndex),
|
||||
0,
|
||||
ZIP_OPSYS_UNIX,
|
||||
static_cast<zip_uint32_t>(S_IFLNK | 0777) << 16) != 0) {
|
||||
*error = zipError(archive);
|
||||
zip_discard(archive);
|
||||
QFile::remove(tempPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (isCanceled()) {
|
||||
zip_discard(archive);
|
||||
QFile::remove(tempPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (zip_close(archive) != 0) {
|
||||
*error = zipError(archive);
|
||||
zip_discard(archive);
|
||||
QFile::remove(tempPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isCanceled() || !QFile::rename(tempPath, m_outputPath)) {
|
||||
QFile::remove(tempPath);
|
||||
if (!isCanceled())
|
||||
*error = QStringLiteral("Could not create %1").arg(m_outputPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
emit progressChanged(100);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ArchiveJob::archiveEntryPath(const QString &archiveName,
|
||||
QString *relativePath,
|
||||
bool *directory) const
|
||||
{
|
||||
QString name = archiveName;
|
||||
name.replace(QLatin1Char('\\'), QLatin1Char('/'));
|
||||
|
||||
*directory = isDirectoryEntry(name);
|
||||
while (name.endsWith(QLatin1Char('/')))
|
||||
name.chop(1);
|
||||
|
||||
if (name.isEmpty()) {
|
||||
relativePath->clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (name.startsWith(QLatin1Char('/')) ||
|
||||
(name.size() >= 2 && name.at(1) == QLatin1Char(':')) ||
|
||||
name.contains(QChar::Null))
|
||||
return false;
|
||||
|
||||
const QStringList components = name.split(QLatin1Char('/'), Qt::SkipEmptyParts);
|
||||
for (const QString &component : components) {
|
||||
if (component == QLatin1String(".."))
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString cleanName = QDir::cleanPath(name);
|
||||
if (cleanName == QLatin1String(".") || cleanName.startsWith(QLatin1String("../")) ||
|
||||
cleanName == QLatin1String("..") || cleanName.startsWith(QLatin1Char('/'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*relativePath = cleanName;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ArchiveJob::zipEntryIsSymbolicLink(zip_t *archive, zip_uint64_t index)
|
||||
{
|
||||
#ifdef Q_OS_UNIX
|
||||
zip_uint8_t operatingSystem = ZIP_OPSYS_UNIX;
|
||||
zip_uint32_t externalAttributes = 0;
|
||||
if (zip_file_get_external_attributes(archive, index, 0,
|
||||
&operatingSystem, &externalAttributes) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (operatingSystem != ZIP_OPSYS_UNIX && operatingSystem != ZIP_OPSYS_OS_X)
|
||||
return false;
|
||||
|
||||
const mode_t mode = static_cast<mode_t>(externalAttributes >> 16);
|
||||
return S_ISLNK(mode);
|
||||
#else
|
||||
Q_UNUSED(archive)
|
||||
Q_UNUSED(index)
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ArchiveJob::extract(QString *error)
|
||||
{
|
||||
if (m_inputs.size() != 1) {
|
||||
*error = QStringLiteral("Only one archive can be extracted at a time");
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString archivePath = m_inputs.first();
|
||||
const QByteArray archivePathBytes = QFile::encodeName(archivePath);
|
||||
int openError = 0;
|
||||
zip_t *archive = zip_open(archivePathBytes.constData(), 0, &openError);
|
||||
if (!archive) {
|
||||
*error = zipOpenError(openError);
|
||||
return false;
|
||||
}
|
||||
|
||||
quint64 total = 0;
|
||||
const zip_int64_t entriesCount = zip_get_num_entries(archive, 0);
|
||||
if (entriesCount < 0) {
|
||||
*error = zipError(archive);
|
||||
zip_discard(archive);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (zip_uint64_t index = 0; index < static_cast<zip_uint64_t>(entriesCount); ++index) {
|
||||
if (isCanceled()) {
|
||||
zip_discard(archive);
|
||||
return false;
|
||||
}
|
||||
|
||||
zip_stat_t stat;
|
||||
zip_stat_init(&stat);
|
||||
if (zip_stat_index(archive, index, 0, &stat) != 0 || !stat.name) {
|
||||
*error = zipError(archive);
|
||||
zip_discard(archive);
|
||||
return false;
|
||||
}
|
||||
|
||||
QString relativePath;
|
||||
bool directory = false;
|
||||
if (!archiveEntryPath(QString::fromUtf8(stat.name), &relativePath, &directory)) {
|
||||
*error = QStringLiteral("Unsafe path in archive: %1").arg(QString::fromUtf8(stat.name));
|
||||
zip_discard(archive);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!directory && !zipEntryIsSymbolicLink(archive, index))
|
||||
total += stat.size;
|
||||
}
|
||||
|
||||
const QString parentPath = QFileInfo(m_outputPath).absolutePath();
|
||||
if (!QDir().exists(parentPath)) {
|
||||
*error = QStringLiteral("The destination folder does not exist: %1").arg(parentPath);
|
||||
zip_discard(archive);
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString tempPath = temporaryPath(parentPath, QStringLiteral("cutefish-extract"));
|
||||
if (!QDir().mkpath(tempPath)) {
|
||||
*error = QStringLiteral("Could not create a temporary extraction folder");
|
||||
zip_discard(archive);
|
||||
return false;
|
||||
}
|
||||
|
||||
quint64 processed = 0;
|
||||
emit progressChanged(0);
|
||||
|
||||
for (zip_uint64_t index = 0; index < static_cast<zip_uint64_t>(entriesCount); ++index) {
|
||||
if (isCanceled()) {
|
||||
zip_discard(archive);
|
||||
QDir(tempPath).removeRecursively();
|
||||
return false;
|
||||
}
|
||||
|
||||
zip_stat_t stat;
|
||||
zip_stat_init(&stat);
|
||||
if (zip_stat_index(archive, index, 0, &stat) != 0 || !stat.name) {
|
||||
*error = zipError(archive);
|
||||
zip_discard(archive);
|
||||
QDir(tempPath).removeRecursively();
|
||||
return false;
|
||||
}
|
||||
|
||||
QString relativePath;
|
||||
bool directory = false;
|
||||
if (!archiveEntryPath(QString::fromUtf8(stat.name), &relativePath, &directory)) {
|
||||
*error = QStringLiteral("Unsafe path in archive: %1").arg(QString::fromUtf8(stat.name));
|
||||
zip_discard(archive);
|
||||
QDir(tempPath).removeRecursively();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (zipEntryIsSymbolicLink(archive, index))
|
||||
continue;
|
||||
|
||||
if (relativePath.isEmpty())
|
||||
continue;
|
||||
|
||||
const QString destinationPath = QDir(tempPath).filePath(relativePath);
|
||||
emit currentFileChanged(relativePath);
|
||||
|
||||
if (directory) {
|
||||
if (!QDir().mkpath(destinationPath)) {
|
||||
*error = QStringLiteral("Could not create %1").arg(relativePath);
|
||||
zip_discard(archive);
|
||||
QDir(tempPath).removeRecursively();
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!QDir().mkpath(QFileInfo(destinationPath).absolutePath())) {
|
||||
*error = QStringLiteral("Could not create the parent folder for %1").arg(relativePath);
|
||||
zip_discard(archive);
|
||||
QDir(tempPath).removeRecursively();
|
||||
return false;
|
||||
}
|
||||
|
||||
zip_file_t *file = zip_fopen_index(archive, index, 0);
|
||||
if (!file) {
|
||||
*error = zipError(archive);
|
||||
zip_discard(archive);
|
||||
QDir(tempPath).removeRecursively();
|
||||
return false;
|
||||
}
|
||||
|
||||
QFile output(destinationPath);
|
||||
if (!output.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||
*error = output.errorString();
|
||||
zip_fclose(file);
|
||||
zip_discard(archive);
|
||||
QDir(tempPath).removeRecursively();
|
||||
return false;
|
||||
}
|
||||
|
||||
char buffer[64 * 1024];
|
||||
while (true) {
|
||||
if (isCanceled()) {
|
||||
output.close();
|
||||
zip_fclose(file);
|
||||
zip_discard(archive);
|
||||
QDir(tempPath).removeRecursively();
|
||||
return false;
|
||||
}
|
||||
|
||||
const zip_int64_t bytesRead = zip_fread(file, buffer, sizeof(buffer));
|
||||
if (bytesRead < 0) {
|
||||
*error = zipError(archive);
|
||||
output.close();
|
||||
zip_fclose(file);
|
||||
zip_discard(archive);
|
||||
QDir(tempPath).removeRecursively();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bytesRead == 0)
|
||||
break;
|
||||
|
||||
if (output.write(buffer, bytesRead) != bytesRead) {
|
||||
*error = output.errorString();
|
||||
output.close();
|
||||
zip_fclose(file);
|
||||
zip_discard(archive);
|
||||
QDir(tempPath).removeRecursively();
|
||||
return false;
|
||||
}
|
||||
|
||||
processed += static_cast<quint64>(bytesRead);
|
||||
emitProgress(processed, total);
|
||||
}
|
||||
|
||||
output.close();
|
||||
if (zip_fclose(file) != 0) {
|
||||
*error = zipError(archive);
|
||||
zip_discard(archive);
|
||||
QDir(tempPath).removeRecursively();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Create symbolic links only after all regular files have been written.
|
||||
// A link inside an archive must never redirect extraction of a later file.
|
||||
for (zip_uint64_t index = 0; index < static_cast<zip_uint64_t>(entriesCount); ++index) {
|
||||
if (isCanceled()) {
|
||||
zip_discard(archive);
|
||||
QDir(tempPath).removeRecursively();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!zipEntryIsSymbolicLink(archive, index))
|
||||
continue;
|
||||
|
||||
zip_stat_t stat;
|
||||
zip_stat_init(&stat);
|
||||
if (zip_stat_index(archive, index, 0, &stat) != 0 || !stat.name) {
|
||||
*error = zipError(archive);
|
||||
zip_discard(archive);
|
||||
QDir(tempPath).removeRecursively();
|
||||
return false;
|
||||
}
|
||||
|
||||
QString relativePath;
|
||||
bool directory = false;
|
||||
if (!archiveEntryPath(QString::fromUtf8(stat.name), &relativePath, &directory) ||
|
||||
directory || relativePath.isEmpty()) {
|
||||
*error = QStringLiteral("Invalid symbolic link path in archive: %1")
|
||||
.arg(QString::fromUtf8(stat.name));
|
||||
zip_discard(archive);
|
||||
QDir(tempPath).removeRecursively();
|
||||
return false;
|
||||
}
|
||||
|
||||
emit currentFileChanged(relativePath);
|
||||
|
||||
const QString destinationPath = QDir(tempPath).filePath(relativePath);
|
||||
if (hasSymbolicLinkParent(tempPath, relativePath)) {
|
||||
*error = QStringLiteral("Symbolic link parent in archive: %1").arg(relativePath);
|
||||
zip_discard(archive);
|
||||
QDir(tempPath).removeRecursively();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!QDir().mkpath(QFileInfo(destinationPath).absolutePath())) {
|
||||
*error = QStringLiteral("Could not create the parent folder for %1").arg(relativePath);
|
||||
zip_discard(archive);
|
||||
QDir(tempPath).removeRecursively();
|
||||
return false;
|
||||
}
|
||||
|
||||
const QFileInfo existing(destinationPath);
|
||||
if (existing.exists() || existing.isSymLink()) {
|
||||
*error = QStringLiteral("Duplicate path in archive: %1").arg(relativePath);
|
||||
zip_discard(archive);
|
||||
QDir(tempPath).removeRecursively();
|
||||
return false;
|
||||
}
|
||||
|
||||
zip_file_t *file = zip_fopen_index(archive, index, 0);
|
||||
if (!file) {
|
||||
*error = zipError(archive);
|
||||
zip_discard(archive);
|
||||
QDir(tempPath).removeRecursively();
|
||||
return false;
|
||||
}
|
||||
|
||||
QByteArray target;
|
||||
char buffer[4096];
|
||||
bool readError = false;
|
||||
while (true) {
|
||||
const zip_int64_t bytesRead = zip_fread(file, buffer, sizeof(buffer));
|
||||
if (bytesRead < 0) {
|
||||
readError = true;
|
||||
break;
|
||||
}
|
||||
if (bytesRead == 0)
|
||||
break;
|
||||
|
||||
if (target.size() > 16 * 1024 - bytesRead) {
|
||||
*error = QStringLiteral("Symbolic link target is too long: %1").arg(relativePath);
|
||||
readError = true;
|
||||
break;
|
||||
}
|
||||
target.append(buffer, static_cast<qsizetype>(bytesRead));
|
||||
}
|
||||
|
||||
const int closeResult = zip_fclose(file);
|
||||
if (readError || closeResult != 0 || target.contains('\0')) {
|
||||
if (readError && error->isEmpty())
|
||||
*error = zipError(archive);
|
||||
if (error->isEmpty())
|
||||
*error = QStringLiteral("Invalid symbolic link target: %1").arg(relativePath);
|
||||
zip_discard(archive);
|
||||
QDir(tempPath).removeRecursively();
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef Q_OS_UNIX
|
||||
if (!QFile::link(QString::fromUtf8(target), destinationPath)) {
|
||||
*error = QStringLiteral("Could not restore symbolic link: %1").arg(relativePath);
|
||||
zip_discard(archive);
|
||||
QDir(tempPath).removeRecursively();
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
*error = QStringLiteral("Symbolic links are not supported on this platform");
|
||||
zip_discard(archive);
|
||||
QDir(tempPath).removeRecursively();
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
zip_discard(archive);
|
||||
|
||||
if (isCanceled() || !QFile::rename(tempPath, m_outputPath)) {
|
||||
QDir(tempPath).removeRecursively();
|
||||
if (!isCanceled())
|
||||
*error = QStringLiteral("Could not create %1").arg(m_outputPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
emit progressChanged(100);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ArchiveJob::zipProgressCallback(zip_t *archive, double progress, void *state)
|
||||
{
|
||||
Q_UNUSED(archive)
|
||||
|
||||
auto *job = static_cast<ArchiveJob *>(state);
|
||||
if (!job)
|
||||
return;
|
||||
|
||||
job->emitProgress(static_cast<quint64>(progress * 100.0), 100);
|
||||
}
|
||||
|
||||
int ArchiveJob::zipCancelCallback(zip_t *archive, void *state)
|
||||
{
|
||||
Q_UNUSED(archive)
|
||||
|
||||
auto *job = static_cast<ArchiveJob *>(state);
|
||||
return job && job->isCanceled() ? 1 : 0;
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
#ifndef ARCHIVEJOB_H
|
||||
#define ARCHIVEJOB_H
|
||||
|
||||
#include <QThread>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#include <zip.h>
|
||||
|
||||
#include <atomic>
|
||||
|
||||
class ArchiveJob : public QThread
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Operation {
|
||||
Compress,
|
||||
Extract,
|
||||
};
|
||||
|
||||
ArchiveJob(Operation operation,
|
||||
const QStringList &inputs,
|
||||
const QString &outputPath,
|
||||
QObject *parent = nullptr);
|
||||
~ArchiveJob() override;
|
||||
|
||||
void cancel();
|
||||
|
||||
signals:
|
||||
void progressChanged(int progress);
|
||||
void currentFileChanged(const QString &fileName);
|
||||
void completed(bool success,
|
||||
bool canceled,
|
||||
const QString &error,
|
||||
const QString &outputPath);
|
||||
|
||||
protected:
|
||||
void run() override;
|
||||
|
||||
private:
|
||||
struct CompressionEntry {
|
||||
QString sourcePath;
|
||||
QString archivePath;
|
||||
bool directory;
|
||||
quint64 size;
|
||||
bool symbolicLink;
|
||||
QString linkTarget;
|
||||
};
|
||||
|
||||
bool compress(QString *error);
|
||||
bool extract(QString *error);
|
||||
|
||||
bool collectCompressionEntries(const QString &sourcePath,
|
||||
const QString &archivePath,
|
||||
QList<CompressionEntry> *entries,
|
||||
QString *error) const;
|
||||
bool archiveEntryPath(const QString &archiveName,
|
||||
QString *relativePath,
|
||||
bool *directory) const;
|
||||
static bool zipEntryIsSymbolicLink(zip_t *archive, zip_uint64_t index);
|
||||
|
||||
void emitProgress(quint64 processed, quint64 total);
|
||||
bool isCanceled() const;
|
||||
|
||||
static void zipProgressCallback(zip_t *archive, double progress, void *state);
|
||||
static int zipCancelCallback(zip_t *archive, void *state);
|
||||
|
||||
Operation m_operation;
|
||||
QStringList m_inputs;
|
||||
QString m_outputPath;
|
||||
std::atomic_bool m_cancelRequested;
|
||||
};
|
||||
|
||||
#endif // ARCHIVEJOB_H
|
||||
@ -0,0 +1,128 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import QtQuick.Window 2.12
|
||||
|
||||
import FishUI 1.0 as FishUI
|
||||
|
||||
FishUI.Window {
|
||||
id: control
|
||||
|
||||
property QtObject archiveModel
|
||||
property Window hostWindow
|
||||
|
||||
flags: Qt.Dialog | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint
|
||||
modality: Qt.NonModal
|
||||
minimizeButtonVisible: false
|
||||
visible: archiveModel !== null && archiveModel.archiveBusy
|
||||
|
||||
width: 420
|
||||
height: 178
|
||||
minimumWidth: width
|
||||
minimumHeight: height
|
||||
maximumWidth: width
|
||||
maximumHeight: height
|
||||
|
||||
x: hostWindow ? hostWindow.x + Math.round((hostWindow.width - width) / 2) : 0
|
||||
y: hostWindow ? hostWindow.y + Math.round((hostWindow.height - height) / 2) : 0
|
||||
|
||||
header.height: 40
|
||||
headerBackground.color: FishUI.Theme.secondBackgroundColor
|
||||
background.color: FishUI.Theme.secondBackgroundColor
|
||||
|
||||
headerItem: Item {
|
||||
Label {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: FishUI.Units.largeSpacing
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - FishUI.Units.largeSpacing * 2
|
||||
text: control.archiveModel === null ? qsTr("File operation")
|
||||
: control.archiveModel.archiveOperation
|
||||
elide: Text.ElideRight
|
||||
font.pointSize: 11
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: FishUI.Units.largeSpacing
|
||||
anchors.rightMargin: FishUI.Units.largeSpacing
|
||||
anchors.topMargin: FishUI.Units.smallSpacing
|
||||
anchors.bottomMargin: FishUI.Units.largeSpacing
|
||||
spacing: FishUI.Units.largeSpacing
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: control.archiveModel === null ? ""
|
||||
: control.archiveModel.archiveCurrentFile
|
||||
visible: text.length > 0
|
||||
elide: Text.ElideMiddle
|
||||
font.pointSize: 10
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 20
|
||||
|
||||
FishUI.BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
width: 20
|
||||
height: 20
|
||||
visible: control.archiveModel !== null
|
||||
&& control.archiveModel.archiveProgress < 0
|
||||
running: visible
|
||||
}
|
||||
|
||||
ProgressBar {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: control.archiveModel !== null
|
||||
&& control.archiveModel.archiveProgress >= 0
|
||||
from: 0
|
||||
to: 100
|
||||
value: control.archiveModel === null ? 0
|
||||
: control.archiveModel.archiveProgress
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Button {
|
||||
text: control.archiveModel !== null && control.archiveModel.archiveCancelling
|
||||
? qsTr("Cancelling…") : qsTr("Cancel")
|
||||
enabled: control.archiveModel !== null
|
||||
&& control.archiveModel.archiveBusy
|
||||
&& !control.archiveModel.archiveCancelling
|
||||
flat: true
|
||||
onClicked: control.archiveModel.cancelArchive()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClosing: function(closeEvent) {
|
||||
if (control.archiveModel !== null && control.archiveModel.archiveBusy) {
|
||||
control.archiveModel.cancelArchive()
|
||||
closeEvent.accepted = false
|
||||
}
|
||||
}
|
||||
|
||||
function updateTransientParent() {
|
||||
if (hostWindow)
|
||||
control.transientParent = hostWindow
|
||||
}
|
||||
|
||||
onHostWindowChanged: updateTransientParent()
|
||||
|
||||
Component.onCompleted: updateTransientParent()
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible)
|
||||
requestActivate()
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue