OSCAR-code/oscar/zip.cpp

352 lines
11 KiB
C++

/* OSCAR ZIP archive creation
* Provides a Qt-convenient wrapper around miniz, see https://github.com/richgel999/miniz
*
* Copyright (c) 2020-2024 The OSCAR Team
*
* This file is subject to the terms and conditions of the GNU General Public
* License. See the file COPYING in the main directory of the source code
* for more details. */
#include "zip.h"
#include <QDebug>
#include <QDateTime>
#include <QCoreApplication>
#include "SleepLib/progressdialog.h"
static const quint64 PROGRESS_SCALE = 1024; // QProgressBar only holds an int, so report progress in KiB.
// Static functions to abstract the details of miniz from the primary logic.
static void* zip_init();
static bool zip_open(void* ctx, QFile & file);
static bool zip_add(void* ctx, const QString & archive_name, const QByteArray & data, const QDateTime & modified);
static void zip_close(void* ctx);
static void zip_done(void* ctx);
ZipFile::ZipFile()
{
m_ctx = zip_init();
}
ZipFile::~ZipFile()
{
Close();
zip_done(m_ctx);
}
bool ZipFile::Open(const QString & filepath)
{
m_file.setFileName(filepath);
bool ok = m_file.open(QIODevice::WriteOnly);
if (!ok) {
qWarning() << "Could not open" << m_file.fileName() << "for writing, error code" << m_file.error() << m_file.errorString();
// qWarning() << "unable to open" << m_file.fileName();
return false;
}
ok = zip_open(m_ctx, m_file);
return ok;
}
void ZipFile::Close()
{
if (m_file.isOpen()) {
zip_close(m_ctx);
m_file.close();
}
}
bool ZipFile::AddDirectory(const QString & path, ProgressDialog* progress)
{
return AddDirectory(path, "", progress);
}
bool ZipFile::AddDirectory(const QString & path, const QString & prefix, ProgressDialog* progress)
{
bool ok;
FileQueue queue;
queue.AddDirectory(path, prefix);
ok = AddFiles(queue, progress);
return ok;
}
bool ZipFile::AddFiles(FileQueue & queue, ProgressDialog* progress)
{
bool ok;
// Exclude the zip file that's being created (if it happens to be in the list).
queue.Remove(QFileInfo(m_file).canonicalFilePath());
qDebug().noquote() << "Adding" << queue.toString();
m_abort = false;
m_progress = 0;
if (progress) {
progress->addAbortButton();
progress->setWindowModality(Qt::ApplicationModal);
progress->open();
connect(this, SIGNAL(setProgressMax(int)), progress, SLOT(setProgressMax(int)));
connect(this, SIGNAL(setProgressValue(int)), progress, SLOT(setProgressValue(int)));
connect(progress, SIGNAL(abortClicked()), this, SLOT(abort()));
}
// Always emit, since the caller may have configured and connected a progress dialog manually.
emit setProgressValue(m_progress/PROGRESS_SCALE);
emit setProgressMax((queue.byteCount() + queue.dirCount())/PROGRESS_SCALE);
QCoreApplication::processEvents();
for (auto & entry : queue.files()) {
ok = AddFile(entry.path, entry.name);
if (!ok || m_abort) {
break;
}
}
if (progress) {
disconnect(progress, SIGNAL(abortClicked()), this, SLOT(abort()));
disconnect(this, SIGNAL(setProgressMax(int)), progress, SLOT(setProgressMax(int)));
disconnect(this, SIGNAL(setProgressValue(int)), progress, SLOT(setProgressValue(int)));
progress->close();
progress->deleteLater();
}
if (!ok) {
qWarning().noquote() << "Unable to create" << m_file.fileName();
Close();
m_file.remove();
} else if (aborted()) {
qDebug().noquote() << "User canceled zip creation.";
Close();
m_file.remove();
} else {
qDebug().noquote() << "Created" << m_file.fileName() << m_file.size() << "bytes";
}
return ok;
}
bool ZipFile::AddFile(const QString & path, const QString & name)
{
if (!m_file.isOpen()) {
qWarning() << m_file.fileName() << "has not been opened for writing";
return false;
}
QFileInfo fi(path);
QByteArray data;
QString archive_name = name;
if (archive_name.isEmpty()) archive_name = fi.fileName();
if (fi.isDir()) {
archive_name += "/";
m_progress += 1;
} else {
// Open and read file into memory.
QFile f(path);
if (!f.open(QIODevice::ReadOnly)) {
qWarning() << path << "can't open";
return false;
}
data = f.readAll();
m_progress += data.size();
}
//qDebug() << "attempting to add" << archive_name << ":" << data.size() << "bytes";
bool ok = zip_add(m_ctx, archive_name, data, fi.lastModified());
emit setProgressValue(m_progress/PROGRESS_SCALE);
QCoreApplication::processEvents();
return ok;
}
// ==================================================================================================
bool FileQueue::AddDirectory(const QString & path, const QString & prefix)
{
QDir dir(path);
if (!dir.exists() || !dir.isReadable()) {
qWarning() << dir.canonicalPath() << "can't read directory";
#if defined(Q_OS_MACOS)
// If this is a directory known to be protected by macOS "Full Disk Access" permissions,
// skip it but don't consider it an error.
static const QSet<QString> s_macProtectedDirs = { ".fseventsd", ".Spotlight-V100", ".Trashes" };
if (s_macProtectedDirs.contains(dir.dirName())) {
return true;
}
#endif
return false;
}
QString base = prefix;
if (base.isEmpty()) base = dir.dirName();
// Add directory entry
bool ok = AddFile(dir.canonicalPath(), base);
if (!ok) {
return false;
}
dir.setFilter(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files | QDir::Hidden);
dir.setSorting(QDir::Name);
QFileInfoList flist = dir.entryInfoList();
for (auto & fi : flist) {
QString canonicalPath = fi.canonicalFilePath();
QString relative_path = base + "/" + fi.fileName();
if (fi.isSymLink()) {
qWarning() << "skipping symlink" << canonicalPath << fi.symLinkTarget();
} else if (fi.isDir()) {
// Descend and recurse
ok &= AddDirectory(canonicalPath, relative_path);
} else {
// Add the file to the zip
ok &= AddFile(canonicalPath, relative_path);
}
// Don't stop in our tracks when we hit an error.
}
return ok;
}
bool FileQueue::AddFile(const QString & path, const QString & prefix)
{
QFileInfo fi(path);
QString canonicalPath = fi.canonicalFilePath();
QString archive_name = prefix;
if (archive_name.isEmpty()) archive_name = fi.fileName();
if (fi.isDir()) {
m_dir_count++;
} else if (fi.exists()) {
m_file_count++;
m_byte_count += fi.size();
} else {
qWarning() << "file doesn't exist" << canonicalPath;
return false;
}
Entry entry = { canonicalPath, archive_name };
m_files.append(entry);
QCoreApplication::processEvents();
return true;
}
int FileQueue::Remove(const QString & path, QString* outName)
{
QFileInfo fi(path);
QString canonicalPath = fi.canonicalFilePath();
int removed = 0;
QMutableListIterator<Entry> i(m_files);
while (i.hasNext()) {
Entry & entry = i.next();
if (entry.path == canonicalPath) {
if (outName) {
// If the caller cares about the name, it will most likely be re-added later rather than skipped.
*outName = entry.name;
} else {
qDebug().noquote() << "skipping file:" << path;
}
if (fi.isDir()) {
m_dir_count--;
} else {
m_file_count--;
m_byte_count -= fi.size();
}
i.remove();
removed++;
}
}
if (removed > 1) {
qWarning().noquote() << removed << "copies found in zip queue:" << path;
}
return removed;
}
const QString FileQueue::toString() const
{
return QString("%1 directories, %2 files, %3 bytes").arg(m_dir_count).arg(m_file_count).arg(m_byte_count);
}
// ==================================================================================================
// Static functions to abstract the details of miniz from the primary logic.
#include "SleepLib/thirdparty/miniz.h"
// Callback for miniz to write compressed data
static size_t zip_write(void *pOpaque, mz_uint64 /*file_ofs*/, const void *pBuf, size_t n)
{
if (pOpaque == nullptr) {
qCritical() << "null pointer passed to ZipFile::Write!";
return 0;
}
QFile* file = (QFile*) pOpaque;
size_t written = file->write((const char*) pBuf, n);
if (written < n) {
qWarning() << "error writing to" << file->fileName();
}
return written;
}
static void* zip_init()
{
mz_zip_archive* pZip = new mz_zip_archive(); // zero-initializes struct
pZip->m_pWrite = zip_write;
return pZip;
}
static void zip_done(void* ctx)
{
Q_ASSERT(ctx);
mz_zip_archive* pZip = (mz_zip_archive*) ctx;
delete pZip;
}
static bool zip_open(void* ctx, QFile & file)
{
Q_ASSERT(ctx);
mz_zip_archive* pZip = (mz_zip_archive*) ctx;
pZip->m_pIO_opaque = &file;
bool ok = mz_zip_writer_init_v2(pZip, 0, MZ_ZIP_FLAG_CASE_SENSITIVE);
if (!ok) {
mz_zip_error mz_err = mz_zip_get_last_error(pZip);
qWarning() << "unable to initialize miniz writer" << MZ_VERSION << mz_zip_get_error_string(mz_err);
}
return ok;
}
static bool zip_add(void* ctx, const QString & archive_name, const QByteArray & data, const QDateTime & modified)
{
Q_ASSERT(ctx);
mz_zip_archive* pZip = (mz_zip_archive*) ctx;
// Add to .zip
time_t last_modified = modified.toTime_t(); // technically deprecated, but miniz expects a time_t
bool ok = mz_zip_writer_add_mem_ex_v2(pZip, archive_name.toLocal8Bit(), data.constData(), data.size(),
nullptr, 0, // no comment
MZ_DEFAULT_COMPRESSION,
0, 0, // not used when compressing data
&last_modified,
nullptr, 0, // no user extra data
nullptr, 0 // no user extra data central
);
if (!ok) {
mz_zip_error mz_err = mz_zip_get_last_error(pZip);
qWarning() << "unable to add" << archive_name << ":" << data.size() << "bytes" << mz_zip_get_error_string(mz_err);
}
return ok;
}
static void zip_close(void* ctx)
{
Q_ASSERT(ctx);
mz_zip_archive* pZip = (mz_zip_archive*) ctx;
mz_zip_writer_finalize_archive(pZip);
mz_zip_writer_end(pZip);
}