diff --git a/kerfuffle/archiveinterface.cpp b/kerfuffle/archiveinterface.cpp index 67281356..dbfe5dca 100644 --- a/kerfuffle/archiveinterface.cpp +++ b/kerfuffle/archiveinterface.cpp @@ -1,294 +1,299 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "archiveinterface.h" #include "ark_debug.h" #include "mimetypes.h" #include #include #include #include namespace Kerfuffle { ReadOnlyArchiveInterface::ReadOnlyArchiveInterface(QObject *parent, const QVariantList & args) : QObject(parent) , m_numberOfVolumes(0) , m_numberOfEntries(0) , m_waitForFinishedSignal(false) , m_isHeaderEncryptionEnabled(false) , m_isCorrupt(false) , m_isMultiVolume(false) { Q_ASSERT(args.size() >= 2); qCDebug(ARK) << "Created read-only interface for" << args.first().toString(); m_filename = args.first().toString(); m_mimetype = determineMimeType(m_filename); connect(this, &ReadOnlyArchiveInterface::entry, this, &ReadOnlyArchiveInterface::onEntry); m_metaData = args.at(1).value(); } ReadOnlyArchiveInterface::~ReadOnlyArchiveInterface() { } void ReadOnlyArchiveInterface::onEntry(Archive::Entry *archiveEntry) { Q_UNUSED(archiveEntry) m_numberOfEntries++; } QString ReadOnlyArchiveInterface::filename() const { return m_filename; } QString ReadOnlyArchiveInterface::comment() const { return m_comment; } bool ReadOnlyArchiveInterface::isReadOnly() const { return true; } bool ReadOnlyArchiveInterface::open() { return true; } void ReadOnlyArchiveInterface::setPassword(const QString &password) { m_password = password; } void ReadOnlyArchiveInterface::setHeaderEncryptionEnabled(bool enabled) { m_isHeaderEncryptionEnabled = enabled; } QString ReadOnlyArchiveInterface::password() const { return m_password; } bool ReadOnlyArchiveInterface::doKill() { //default implementation return false; } bool ReadOnlyArchiveInterface::doSuspend() { //default implementation return false; } bool ReadOnlyArchiveInterface::doResume() { //default implementation return false; } void ReadOnlyArchiveInterface::setCorrupt(bool isCorrupt) { m_isCorrupt = isCorrupt; } bool ReadOnlyArchiveInterface::isCorrupt() const { return m_isCorrupt; } bool ReadOnlyArchiveInterface::isMultiVolume() const { return m_isMultiVolume; } void ReadOnlyArchiveInterface::setMultiVolume(bool value) { m_isMultiVolume = value; } int ReadOnlyArchiveInterface::numberOfVolumes() const { return m_numberOfVolumes; } QString ReadOnlyArchiveInterface::multiVolumeName() const { return filename(); } ReadWriteArchiveInterface::ReadWriteArchiveInterface(QObject *parent, const QVariantList &args) : ReadOnlyArchiveInterface(parent, args) { qCDebug(ARK) << "Created read-write interface for" << args.first().toString(); connect(this, &ReadWriteArchiveInterface::entryRemoved, this, &ReadWriteArchiveInterface::onEntryRemoved); } ReadWriteArchiveInterface::~ReadWriteArchiveInterface() { } bool ReadOnlyArchiveInterface::waitForFinishedSignal() { return m_waitForFinishedSignal; } int ReadOnlyArchiveInterface::moveRequiredSignals() const { return 1; } int ReadOnlyArchiveInterface::copyRequiredSignals() const { return 1; } void ReadOnlyArchiveInterface::setWaitForFinishedSignal(bool value) { m_waitForFinishedSignal = value; } QStringList ReadOnlyArchiveInterface::entryFullPaths(const QVector &entries, PathFormat format) { QStringList filesList; foreach (const Archive::Entry *file, entries) { filesList << file->fullPath(format); } return filesList; } QVector ReadOnlyArchiveInterface::entriesWithoutChildren(const QVector &entries) { // QMap is easy way to get entries sorted by their fullPath. QMap sortedEntries; foreach (Archive::Entry *entry, entries) { sortedEntries.insert(entry->fullPath(), entry); } QVector filteredEntries; QString lastFolder; foreach (Archive::Entry *entry, sortedEntries) { if (lastFolder.count() > 0 && entry->fullPath().startsWith(lastFolder)) { continue; } lastFolder = (entry->fullPath().right(1) == QLatin1String("/")) ? entry->fullPath() : QString(); filteredEntries << entry; } return filteredEntries; } QStringList ReadOnlyArchiveInterface::entryPathsFromDestination(QStringList entries, const Archive::Entry *destination, int entriesWithoutChildren) { QStringList paths = QStringList(); entries.sort(); QString lastFolder; const QString destinationPath = (destination == Q_NULLPTR) ? QString() : destination->fullPath(); QString newPath; int nameLength = 0; foreach (const QString &entryPath, entries) { if (lastFolder.count() > 0 && entryPath.startsWith(lastFolder)) { // Replace last moved or copied folder path with destination path. int charsCount = entryPath.count() - lastFolder.count(); if (entriesWithoutChildren != 1) { charsCount += nameLength; } newPath = destinationPath + entryPath.right(charsCount); } else { const QString name = entryPath.split(QLatin1Char('/'), QString::SkipEmptyParts).last(); if (entriesWithoutChildren != 1) { newPath = destinationPath + name; if (entryPath.right(1) == QLatin1String("/")) { newPath += QLatin1Char('/'); } } else { // If the mode is set to Move and there is only one passed file in the list, // we have to use destination as newPath. newPath = destinationPath; } if (entryPath.right(1) == QLatin1String("/")) { nameLength = name.count() + 1; // plus slash lastFolder = entryPath; } else { nameLength = 0; lastFolder = QString(); } } paths << newPath; } return paths; } bool ReadOnlyArchiveInterface::isHeaderEncryptionEnabled() const { return m_isHeaderEncryptionEnabled; } QMimeType ReadOnlyArchiveInterface::mimetype() const { return m_mimetype; } +bool ReadOnlyArchiveInterface::hasBatchExtractionProgress() const +{ + return false; +} + bool ReadWriteArchiveInterface::isReadOnly() const { // We set corrupt archives to read-only to avoid add/delete actions, that // are likely to fail anyway. if (isCorrupt()) { return true; } QFileInfo fileInfo(filename()); if (fileInfo.exists()) { return !fileInfo.isWritable(); } else { return !fileInfo.dir().exists(); // TODO: Should also check if we can create a file in that directory } } int ReadOnlyArchiveInterface::numberOfEntries() const { return m_numberOfEntries; } void ReadWriteArchiveInterface::onEntryRemoved(const QString &path) { Q_UNUSED(path) m_numberOfEntries--; } } // namespace Kerfuffle diff --git a/kerfuffle/archiveinterface.h b/kerfuffle/archiveinterface.h index 64f9177b..04998ff7 100644 --- a/kerfuffle/archiveinterface.h +++ b/kerfuffle/archiveinterface.h @@ -1,234 +1,239 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef ARCHIVEINTERFACE_H #define ARCHIVEINTERFACE_H #include "archive_kerfuffle.h" #include "archive_entry.h" #include "kerfuffle_export.h" #include "kerfuffle/archiveentry.h" #include #include #include #include namespace Kerfuffle { class Query; class KERFUFFLE_EXPORT ReadOnlyArchiveInterface: public QObject { Q_OBJECT public: explicit ReadOnlyArchiveInterface(QObject *parent, const QVariantList &args); virtual ~ReadOnlyArchiveInterface(); /** * Returns the filename of the archive currently being handled. */ QString filename() const; /** * Returns the comment of the archive. */ QString comment() const; /** * @return The password of the archive, if any. */ QString password() const; bool isMultiVolume() const; int numberOfVolumes() const; /** * Returns whether the file can only be read. * * @return @c true The file cannot be written. * @return @c false The file can be read and written. */ virtual bool isReadOnly() const; virtual bool open(); /** * List archive contents. * This runs the process of reading archive contents. * When subclassing, you can block as long as you need (unless you called setWaitForFinishedSignal(true)). * @returns whether the listing succeeded. * @note If returning false, make sure to emit the error() signal beforewards to notify * the user of the error condition. */ virtual bool list() = 0; virtual bool testArchive() = 0; void setPassword(const QString &password); void setHeaderEncryptionEnabled(bool enabled); /** * Extract files from archive. * Globally recognized extraction options: * @li PreservePaths - preserve file paths (extract flat if false) * @li RootNode - node in the archive which will correspond to the @arg destinationDirectory * When subclassing, you can block as long as you need (unless you called setWaitForFinishedSignal(true)). * @returns whether the listing succeeded. * @note If returning false, make sure to emit the error() signal beforewards to notify * the user of the error condition. */ virtual bool extractFiles(const QVector &files, const QString &destinationDirectory, const ExtractionOptions &options) = 0; bool waitForFinishedSignal(); /** * Returns count of required finish signals for a job to be finished. * * These two methods are used by move and copy jobs, which in some plugins implementations have to call * several processes sequentually. For instance, moving entries in zip archive is only possible if * extracting the entries, deleting them, recreating destination folder structure and adding them back again. */ virtual int moveRequiredSignals() const; virtual int copyRequiredSignals() const; /** * Returns the list of filenames retrieved from the list of entries. */ static QStringList entryFullPaths(const QVector &entries, PathFormat format = WithTrailingSlash); /** * Returns the list of the entries, excluding their children. * * This method relies on entries paths so doesn't require parents to be set. */ static QVector entriesWithoutChildren(const QVector &entries); /** * Returns the string list of entry paths, which will be a result of adding/moving/copying entries. * * @param entries The entries which will be added/moved/copied. * @param destination Destination path within the archive to which entries have to be added. For renaming an entry * the path has to contain a new filename too. * @param entriesWithoutChildren Entries count, excluding their children. For AddJob or CopyJob 0 MUST be passed. * * @return For entries * some/dir/ * some/dir/entry * some/dir/some/entry * some/another/entry * and destination * some/destination * will return * some/destination/dir/ * some/destination/dir/entry * some/destination/dir/some/enty * some/destination/entry */ static QStringList entryPathsFromDestination(QStringList entries, const Archive::Entry *destination, int entriesWithoutChildren); virtual bool doKill(); virtual bool doSuspend(); virtual bool doResume(); bool isHeaderEncryptionEnabled() const; virtual QString multiVolumeName() const; void setMultiVolume(bool value); int numberOfEntries() const; QMimeType mimetype() const; + /** + * @return Whether the interface supports progress reporting for BatchExtractJobs. + */ + virtual bool hasBatchExtractionProgress() const; + signals: void cancelled(); void error(const QString &message, const QString &details = QString()); void entry(Archive::Entry *archiveEntry); void progress(double progress); void info(const QString &info); void finished(bool result); void testSuccess(); void compressionMethodFound(const QString &method); void encryptionMethodFound(const QString &method); /** * Emitted when @p query needs to be executed on the GUI thread. */ void userQuery(Query *query); protected: /** * Setting this option to true will not run the functions in their own thread. * Instead it will be necessary to call finished(bool) when the operation is actually finished. */ void setWaitForFinishedSignal(bool value); void setCorrupt(bool isCorrupt); bool isCorrupt() const; QString m_comment; int m_numberOfVolumes; int m_numberOfEntries; KPluginMetaData m_metaData; private: QString m_filename; QMimeType m_mimetype; QString m_password; bool m_waitForFinishedSignal; bool m_isHeaderEncryptionEnabled; bool m_isCorrupt; bool m_isMultiVolume; private slots: void onEntry(Archive::Entry *archiveEntry); }; class KERFUFFLE_EXPORT ReadWriteArchiveInterface: public ReadOnlyArchiveInterface { Q_OBJECT public: enum OperationMode { List, Extract, Add, Move, Copy, Delete, Comment, Test }; explicit ReadWriteArchiveInterface(QObject *parent, const QVariantList &args); virtual ~ReadWriteArchiveInterface(); bool isReadOnly() const Q_DECL_OVERRIDE; virtual bool addFiles(const QVector &files, const Archive::Entry *destination, const CompressionOptions& options, uint numberOfEntriesToAdd = 0) = 0; virtual bool moveFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions& options) = 0; virtual bool copyFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions& options) = 0; virtual bool deleteFiles(const QVector &files) = 0; virtual bool addComment(const QString &comment) = 0; signals: void entryRemoved(const QString &path); private slots: void onEntryRemoved(const QString &path); }; } // namespace Kerfuffle #endif // ARCHIVEINTERFACE_H diff --git a/kerfuffle/jobs.cpp b/kerfuffle/jobs.cpp index a85c7efb..8cd2d114 100644 --- a/kerfuffle/jobs.cpp +++ b/kerfuffle/jobs.cpp @@ -1,811 +1,831 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "jobs.h" #include "archiveentry.h" #include "ark_debug.h" #include #include #include #include #include #include #include #include #include namespace Kerfuffle { class Job::Private : public QThread { Q_OBJECT public: Private(Job *job, QObject *parent = 0) : QThread(parent) , q(job) { } virtual void run() Q_DECL_OVERRIDE; private: Job *q; }; void Job::Private::run() { q->doWork(); } Job::Job(Archive *archive, ReadOnlyArchiveInterface *interface) : KJob() , m_archive(archive) , m_archiveInterface(interface) , d(new Private(this)) { setCapabilities(KJob::Killable); } Job::Job(Archive *archive) : Job(archive, Q_NULLPTR) {} Job::Job(ReadOnlyArchiveInterface *interface) : Job(Q_NULLPTR, interface) {} Job::~Job() { qDeleteAll(m_archiveEntries); m_archiveEntries.clear(); if (d->isRunning()) { d->wait(); } delete d; } ReadOnlyArchiveInterface *Job::archiveInterface() { // Use the archive interface. if (archive()) { return archive()->interface(); } // Use the interface passed to this job (e.g. JSONArchiveInterface in jobstest.cpp). return m_archiveInterface; } Archive *Job::archive() const { return m_archive; } QString Job::errorString() const { if (!errorText().isEmpty()) { return errorText(); } if (archive()) { if (archive()->error() == NoPlugin) { return i18n("No suitable plugin found. Ark does not seem to support this file type."); } if (archive()->error() == FailedPlugin) { return i18n("Failed to load a suitable plugin. Make sure any executables needed to handle the archive type are installed."); } } return QString(); } void Job::start() { jobTimer.start(); // We have an archive but it's not valid, nothing to do. if (archive() && !archive()->isValid()) { QTimer::singleShot(0, this, [=]() { onFinished(false); }); return; } if (archiveInterface()->waitForFinishedSignal()) { // CLI-based interfaces run a QProcess, no need to use threads. QTimer::singleShot(0, this, &Job::doWork); } else { // Run the job in another thread. d->start(); } } void Job::connectToArchiveInterfaceSignals() { connect(archiveInterface(), &ReadOnlyArchiveInterface::cancelled, this, &Job::onCancelled); connect(archiveInterface(), &ReadOnlyArchiveInterface::error, this, &Job::onError); connect(archiveInterface(), &ReadOnlyArchiveInterface::entry, this, &Job::onEntry); connect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &Job::onProgress); connect(archiveInterface(), &ReadOnlyArchiveInterface::info, this, &Job::onInfo); connect(archiveInterface(), &ReadOnlyArchiveInterface::finished, this, &Job::onFinished); connect(archiveInterface(), &ReadOnlyArchiveInterface::userQuery, this, &Job::onUserQuery); auto readWriteInterface = qobject_cast(archiveInterface()); if (readWriteInterface) { connect(readWriteInterface, &ReadWriteArchiveInterface::entryRemoved, this, &Job::onEntryRemoved); } } void Job::onCancelled() { qCDebug(ARK) << "Cancelled emitted"; setError(KJob::KilledJobError); } void Job::onError(const QString & message, const QString & details) { Q_UNUSED(details) qCDebug(ARK) << "Error emitted:" << message; setError(KJob::UserDefinedError); setErrorText(message); } void Job::onEntry(Archive::Entry *entry) { emit newEntry(entry); } void Job::onProgress(double value) { setPercent(static_cast(100.0*value)); } void Job::onInfo(const QString& info) { emit infoMessage(this, info); } void Job::onEntryRemoved(const QString & path) { emit entryRemoved(path); } void Job::onFinished(bool result) { qCDebug(ARK) << "Job finished, result:" << result << ", time:" << jobTimer.elapsed() << "ms"; if (archive() && !archive()->isValid()) { setError(KJob::UserDefinedError); } emitResult(); } void Job::onUserQuery(Query *query) { if (archiveInterface()->waitForFinishedSignal()) { qCWarning(ARK) << "Plugins run from the main thread should call directly query->execute()"; } emit userQuery(query); } bool Job::doKill() { if (d->isRunning()) { d->requestInterruption(); d->wait(); } bool ret = archiveInterface()->doKill(); if (!ret) { qCWarning(ARK) << "Killing does not seem to be supported here."; } return ret; } LoadJob::LoadJob(Archive *archive, ReadOnlyArchiveInterface *interface) : Job(archive, interface) , m_isSingleFolderArchive(true) , m_isPasswordProtected(false) , m_extractedFilesSize(0) , m_dirCount(0) , m_filesCount(0) { qCDebug(ARK) << "LoadJob created"; connect(this, &LoadJob::newEntry, this, &LoadJob::onNewEntry); } LoadJob::LoadJob(Archive *archive) : LoadJob(archive, Q_NULLPTR) {} LoadJob::LoadJob(ReadOnlyArchiveInterface *interface) : LoadJob(Q_NULLPTR, interface) {} void LoadJob::doWork() { emit description(this, i18n("Loading archive"), qMakePair(i18n("Archive"), archiveInterface()->filename())); connectToArchiveInterfaceSignals(); bool ret = archiveInterface()->list(); if (!archiveInterface()->waitForFinishedSignal()) { // onFinished() needs to be called after onNewEntry(), because the former reads members set in the latter. // So we need to put it in the event queue, just like the single-thread case does by emitting finished(). QTimer::singleShot(0, this, [=]() { onFinished(ret); }); } } void LoadJob::onFinished(bool result) { if (archive()) { archive()->setProperty("unpackedSize", extractedFilesSize()); archive()->setProperty("isSingleFolder", isSingleFolderArchive()); const auto name = subfolderName().isEmpty() ? archive()->completeBaseName() : subfolderName(); archive()->setProperty("subfolderName", name); if (isPasswordProtected()) { archive()->setProperty("encryptionType", archive()->password().isEmpty() ? Archive::Encrypted : Archive::HeaderEncrypted); } } Job::onFinished(result); } qlonglong LoadJob::extractedFilesSize() const { return m_extractedFilesSize; } bool LoadJob::isPasswordProtected() const { return m_isPasswordProtected; } bool LoadJob::isSingleFolderArchive() const { if (m_filesCount == 1 && m_dirCount == 0) { return false; } return m_isSingleFolderArchive; } void LoadJob::onNewEntry(const Archive::Entry *entry) { m_extractedFilesSize += entry->property("size").toLongLong(); m_isPasswordProtected |= entry->property("isPasswordProtected").toBool(); if (entry->isDir()) { m_dirCount++; } else { m_filesCount++; } if (m_isSingleFolderArchive) { // RPM filenames have the ./ prefix, and "." would be detected as the subfolder name, so we remove it. const QString fullPath = entry->fullPath().replace(QRegularExpression(QStringLiteral("^\\./")), QString()); const QString basePath = fullPath.split(QLatin1Char('/')).at(0); if (m_basePath.isEmpty()) { m_basePath = basePath; m_subfolderName = basePath; } else { if (m_basePath != basePath) { m_isSingleFolderArchive = false; m_subfolderName.clear(); } } } } QString LoadJob::subfolderName() const { if (!isSingleFolderArchive()) { return QString(); } return m_subfolderName; } BatchExtractJob::BatchExtractJob(LoadJob *loadJob, const QString &destination, bool autoSubfolder, bool preservePaths) : Job(loadJob->archive()) , m_loadJob(loadJob) , m_destination(destination) , m_autoSubfolder(autoSubfolder) , m_preservePaths(preservePaths) { qCDebug(ARK) << "BatchExtractJob created"; } void BatchExtractJob::doWork() { connect(m_loadJob, &KJob::result, this, &BatchExtractJob::slotLoadingFinished); - // progress() will be actually emitted by the ExtractJob, but the archiveInterface() is the same. - connect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &BatchExtractJob::onProgress); + if (archiveInterface()->hasBatchExtractionProgress()) { + // progress() will be actually emitted by the LoadJob, but the archiveInterface() is the same. + connect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &BatchExtractJob::slotLoadingProgress); + } // Forward LoadJob's signals. connect(m_loadJob, &Kerfuffle::Job::newEntry, this, &BatchExtractJob::newEntry); connect(m_loadJob, &Kerfuffle::Job::userQuery, this, &BatchExtractJob::userQuery); m_loadJob->start(); } +void BatchExtractJob::slotLoadingProgress(double progress) +{ + // Progress from LoadJob counts only for 50% of the BatchExtractJob's duration. + m_lastPercentage = static_cast(50.0*progress); + setPercent(m_lastPercentage); +} + +void BatchExtractJob::slotExtractProgress(double progress) +{ + // The 2nd 50% of the BatchExtractJob's duration comes from the ExtractJob. + setPercent(m_lastPercentage + static_cast(50.0*progress)); +} + void BatchExtractJob::slotLoadingFinished(KJob *job) { if (job->error()) { // Forward errors as well. onError(job->errorString(), QString()); onFinished(false); return; } // Now we can start extraction. setupDestination(); Kerfuffle::ExtractionOptions options; options.setPreservePaths(m_preservePaths); auto extractJob = archive()->extractFiles({}, m_destination, options); if (extractJob) { connect(extractJob, &KJob::result, this, &BatchExtractJob::emitResult); connect(extractJob, &Kerfuffle::Job::userQuery, this, &BatchExtractJob::userQuery); + if (archiveInterface()->hasBatchExtractionProgress()) { + // The LoadJob is done, change slot and start setting the percentage from m_lastPercentage on. + disconnect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &BatchExtractJob::slotLoadingProgress); + connect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &BatchExtractJob::slotExtractProgress); + } extractJob->start(); } else { emitResult(); } } void BatchExtractJob::setupDestination() { const bool isSingleFolderRPM = (archive()->isSingleFolder() && (archive()->mimeType().name() == QLatin1String("application/x-rpm"))); if (m_autoSubfolder && (!archive()->isSingleFolder() || isSingleFolderRPM)) { const QDir d(m_destination); QString subfolderName = archive()->subfolderName(); // Special case for single folder RPM archives. // We don't want the autodetected folder to have a meaningless "usr" name. if (isSingleFolderRPM && subfolderName == QStringLiteral("usr")) { qCDebug(ARK) << "Detected single folder RPM archive. Using archive basename as subfolder name"; subfolderName = QFileInfo(archive()->fileName()).completeBaseName(); } if (d.exists(subfolderName)) { subfolderName = KIO::suggestName(QUrl::fromUserInput(m_destination, QString(), QUrl::AssumeLocalFile), subfolderName); } d.mkdir(subfolderName); m_destination += QLatin1Char( '/' ) + subfolderName; } } CreateJob::CreateJob(Archive *archive, const QVector &entries, const CompressionOptions &options) : Job(archive) , m_entries(entries) , m_options(options) { qCDebug(ARK) << "CreateJob created"; } void CreateJob::enableEncryption(const QString &password, bool encryptHeader) { archive()->encrypt(password, encryptHeader); } void CreateJob::setMultiVolume(bool isMultiVolume) { archive()->setMultiVolume(isMultiVolume); } void CreateJob::doWork() { auto addJob = archive()->addFiles(m_entries, new Archive::Entry(this), m_options); if (addJob) { connect(addJob, &KJob::result, this, &CreateJob::emitResult); // Forward description signal from AddJob, we need to change the first argument ('this' needs to be a CreateJob). connect(addJob, &KJob::description, this, [=](KJob *, const QString &title, const QPair &field1, const QPair &) { emit description(this, title, field1); }); addJob->start(); } else { emitResult(); } } ExtractJob::ExtractJob(const QVector &entries, const QString &destinationDir, const ExtractionOptions &options, ReadOnlyArchiveInterface *interface) : Job(interface) , m_entries(entries) , m_destinationDir(destinationDir) , m_options(options) { qCDebug(ARK) << "ExtractJob created"; } void ExtractJob::doWork() { QString desc; if (m_entries.count() == 0) { desc = i18n("Extracting all files"); } else { desc = i18np("Extracting one file", "Extracting %1 files", m_entries.count()); } emit description(this, desc, qMakePair(i18n("Archive"), archiveInterface()->filename())); QFileInfo destDirInfo(m_destinationDir); if (destDirInfo.isDir() && (!destDirInfo.isWritable() || !destDirInfo.isExecutable())) { onError(xi18n("Could not write to destination %1.Check whether you have sufficient permissions.", m_destinationDir), QString()); onFinished(false); return; } connectToArchiveInterfaceSignals(); qCDebug(ARK) << "Starting extraction with" << m_entries.count() << "selected files." << m_entries << "Destination dir:" << m_destinationDir << "Options:" << m_options; bool ret = archiveInterface()->extractFiles(m_entries, m_destinationDir, m_options); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } QString ExtractJob::destinationDirectory() const { return m_destinationDir; } ExtractionOptions ExtractJob::extractionOptions() const { return m_options; } TempExtractJob::TempExtractJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : Job(interface) , m_entry(entry) , m_passwordProtectedHint(passwordProtectedHint) { m_tmpExtractDir = new QTemporaryDir(); } QString TempExtractJob::validatedFilePath() const { QString path = extractionDir() + QLatin1Char('/') + m_entry->fullPath(); // Make sure a maliciously crafted archive with parent folders named ".." do // not cause the previewed file path to be located outside the temporary // directory, resulting in a directory traversal issue. path.remove(QStringLiteral("../")); return path; } ExtractionOptions TempExtractJob::extractionOptions() const { ExtractionOptions options; if (m_passwordProtectedHint) { options.setEncryptedArchiveHint(true); } return options; } QTemporaryDir *TempExtractJob::tempDir() const { return m_tmpExtractDir; } void TempExtractJob::doWork() { emit description(this, i18n("Extracting one file")); connectToArchiveInterfaceSignals(); qCDebug(ARK) << "Extracting:" << m_entry; bool ret = archiveInterface()->extractFiles({m_entry}, extractionDir(), extractionOptions()); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } QString TempExtractJob::extractionDir() const { return m_tmpExtractDir->path(); } PreviewJob::PreviewJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : TempExtractJob(entry, passwordProtectedHint, interface) { qCDebug(ARK) << "PreviewJob created"; } OpenJob::OpenJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : TempExtractJob(entry, passwordProtectedHint, interface) { qCDebug(ARK) << "OpenJob created"; } OpenWithJob::OpenWithJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : OpenJob(entry, passwordProtectedHint, interface) { qCDebug(ARK) << "OpenWithJob created"; } AddJob::AddJob(const QVector &entries, const Archive::Entry *destination, const CompressionOptions& options, ReadWriteArchiveInterface *interface) : Job(interface) , m_entries(entries) , m_destination(destination) , m_options(options) { qCDebug(ARK) << "AddJob created"; } void AddJob::doWork() { // Set current dir. const QString globalWorkDir = m_options.globalWorkDir(); const QDir workDir = globalWorkDir.isEmpty() ? QDir::current() : QDir(globalWorkDir); if (!globalWorkDir.isEmpty()) { qCDebug(ARK) << "GlobalWorkDir is set, changing dir to " << globalWorkDir; m_oldWorkingDir = QDir::currentPath(); QDir::setCurrent(globalWorkDir); } // Count total number of entries to be added. qulonglong totalCount = 0; QElapsedTimer timer; timer.start(); foreach (const Archive::Entry* entry, m_entries) { totalCount++; if (QFileInfo(entry->fullPath()).isDir()) { QDirIterator it(entry->fullPath(), QDir::AllEntries | QDir::Readable | QDir::Hidden | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (it.hasNext()) { it.next(); totalCount++; } } } qCDebug(ARK) << "AddJob: going to add" << totalCount << "entries, counted in" << timer.elapsed() << "ms"; QString desc = i18np("Adding a file", "Adding %1 files", totalCount); emit description(this, desc, qMakePair(i18n("Archive"), archiveInterface()->filename())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); // The file paths must be relative to GlobalWorkDir. foreach (Archive::Entry *entry, m_entries) { // #191821: workDir must be used instead of QDir::current() // so that symlinks aren't resolved automatically const QString &fullPath = entry->fullPath(); QString relativePath = workDir.relativeFilePath(fullPath); if (fullPath.endsWith(QLatin1Char('/'))) { relativePath += QLatin1Char('/'); } entry->setFullPath(relativePath); } connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->addFiles(m_entries, m_destination, m_options, totalCount); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void AddJob::onFinished(bool result) { if (!m_oldWorkingDir.isEmpty()) { QDir::setCurrent(m_oldWorkingDir); } Job::onFinished(result); } MoveJob::MoveJob(const QVector &entries, Archive::Entry *destination, const CompressionOptions& options , ReadWriteArchiveInterface *interface) : Job(interface) , m_finishedSignalsCount(0) , m_entries(entries) , m_destination(destination) , m_options(options) { qCDebug(ARK) << "MoveJob created"; } void MoveJob::doWork() { qCDebug(ARK) << "MoveJob: going to move" << m_entries.count() << "file(s)"; QString desc = i18np("Moving a file", "Moving %1 files", m_entries.count()); emit description(this, desc, qMakePair(i18n("Archive"), archiveInterface()->filename())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->moveFiles(m_entries, m_destination, m_options); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void MoveJob::onFinished(bool result) { m_finishedSignalsCount++; if (m_finishedSignalsCount == archiveInterface()->moveRequiredSignals()) { Job::onFinished(result); } } CopyJob::CopyJob(const QVector &entries, Archive::Entry *destination, const CompressionOptions &options, ReadWriteArchiveInterface *interface) : Job(interface) , m_finishedSignalsCount(0) , m_entries(entries) , m_destination(destination) , m_options(options) { qCDebug(ARK) << "CopyJob created"; } void CopyJob::doWork() { qCDebug(ARK) << "CopyJob: going to copy" << m_entries.count() << "file(s)"; QString desc = i18np("Copying a file", "Copying %1 files", m_entries.count()); emit description(this, desc, qMakePair(i18n("Archive"), archiveInterface()->filename())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->copyFiles(m_entries, m_destination, m_options); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void CopyJob::onFinished(bool result) { m_finishedSignalsCount++; if (m_finishedSignalsCount == archiveInterface()->copyRequiredSignals()) { Job::onFinished(result); } } DeleteJob::DeleteJob(const QVector &entries, ReadWriteArchiveInterface *interface) : Job(interface) , m_entries(entries) { } void DeleteJob::doWork() { QString desc = i18np("Deleting a file from the archive", "Deleting %1 files", m_entries.count()); emit description(this, desc, qMakePair(i18n("Archive"), archiveInterface()->filename())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->deleteFiles(m_entries); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } CommentJob::CommentJob(const QString& comment, ReadWriteArchiveInterface *interface) : Job(interface) , m_comment(comment) { } void CommentJob::doWork() { emit description(this, i18n("Adding comment")); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->addComment(m_comment); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } TestJob::TestJob(ReadOnlyArchiveInterface *interface) : Job(interface) { m_testSuccess = false; } void TestJob::doWork() { qCDebug(ARK) << "TestJob started"; emit description(this, i18n("Testing archive"), qMakePair(i18n("Archive"), archiveInterface()->filename())); connectToArchiveInterfaceSignals(); connect(archiveInterface(), &ReadOnlyArchiveInterface::testSuccess, this, &TestJob::onTestSuccess); bool ret = archiveInterface()->testArchive(); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void TestJob::onTestSuccess() { m_testSuccess = true; } bool TestJob::testSucceeded() { return m_testSuccess; } } // namespace Kerfuffle #include "jobs.moc" diff --git a/kerfuffle/jobs.h b/kerfuffle/jobs.h index d5f0db6b..174b55f7 100644 --- a/kerfuffle/jobs.h +++ b/kerfuffle/jobs.h @@ -1,416 +1,419 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef JOBS_H #define JOBS_H #include "kerfuffle_export.h" #include "archiveinterface.h" #include "archive_kerfuffle.h" #include "archiveentry.h" #include "queries.h" #include #include #include namespace Kerfuffle { class KERFUFFLE_EXPORT Job : public KJob { Q_OBJECT public: /** * @return The archive processed by this job. * @warning This method should not be called before start(). */ Archive *archive() const; QString errorString() const Q_DECL_OVERRIDE; void start() Q_DECL_OVERRIDE; protected: Job(Archive *archive, ReadOnlyArchiveInterface *interface); Job(Archive *archive); Job(ReadOnlyArchiveInterface *interface); virtual ~Job(); virtual bool doKill() Q_DECL_OVERRIDE; ReadOnlyArchiveInterface *archiveInterface(); QVector m_archiveEntries; void connectToArchiveInterfaceSignals(); public slots: virtual void doWork() = 0; protected slots: virtual void onCancelled(); virtual void onError(const QString &message, const QString &details); virtual void onInfo(const QString &info); virtual void onEntry(Archive::Entry *entry); virtual void onProgress(double progress); virtual void onEntryRemoved(const QString &path); virtual void onFinished(bool result); virtual void onUserQuery(Query *query); signals: void entryRemoved(const QString & entry); void newEntry(Archive::Entry*); void userQuery(Kerfuffle::Query*); private: Archive *m_archive; ReadOnlyArchiveInterface *m_archiveInterface; QElapsedTimer jobTimer; class Private; Private * const d; }; /** * Load an existing archive. * Example usage: * * \code * * auto job = Archive::load(filename); * connect(job, &KJob::result, [](KJob *job) { * if (!job->error) { * auto archive = qobject_cast(job)->archive(); * // do something with archive. * } * }); * job->start(); * * \endcode */ class KERFUFFLE_EXPORT LoadJob : public Job { Q_OBJECT public: explicit LoadJob(Archive *archive); explicit LoadJob(ReadOnlyArchiveInterface *interface); qlonglong extractedFilesSize() const; bool isPasswordProtected() const; bool isSingleFolderArchive() const; QString subfolderName() const; public slots: virtual void doWork() Q_DECL_OVERRIDE; protected slots: virtual void onFinished(bool result) Q_DECL_OVERRIDE; private: explicit LoadJob(Archive *archive, ReadOnlyArchiveInterface *interface); bool m_isSingleFolderArchive; bool m_isPasswordProtected; QString m_subfolderName; QString m_basePath; qlonglong m_extractedFilesSize; qlonglong m_dirCount; qlonglong m_filesCount; private slots: void onNewEntry(const Archive::Entry*); }; /** * Perform a batch extraction of an existing archive. * Internally it runs a LoadJob before the actual extraction, * to figure out properties such as the subfolder name. */ class KERFUFFLE_EXPORT BatchExtractJob : public Job { Q_OBJECT public: explicit BatchExtractJob(LoadJob *loadJob, const QString &destination, bool autoSubfolder, bool preservePaths); signals: void newEntry(Archive::Entry *entry); void userQuery(Query *query); public slots: virtual void doWork() Q_DECL_OVERRIDE; private slots: + void slotLoadingProgress(double progress); + void slotExtractProgress(double progress); void slotLoadingFinished(KJob *job); private: void setupDestination(); LoadJob *m_loadJob; QString m_destination; bool m_autoSubfolder; bool m_preservePaths; + unsigned long m_lastPercentage = 0; }; /** * Create a new archive given a bunch of entries. */ class KERFUFFLE_EXPORT CreateJob : public Job { Q_OBJECT public: explicit CreateJob(Archive *archive, const QVector &entries, const CompressionOptions& options); /** * @param password The password to encrypt the archive with. * @param encryptHeader Whether to encrypt also the list of files. */ void enableEncryption(const QString &password, bool encryptHeader); /** * Set whether the new archive should be multivolume. */ void setMultiVolume(bool isMultiVolume); public slots: virtual void doWork() Q_DECL_OVERRIDE; private: QVector m_entries; CompressionOptions m_options; }; class KERFUFFLE_EXPORT ExtractJob : public Job { Q_OBJECT public: ExtractJob(const QVector &entries, const QString& destinationDir, const ExtractionOptions& options, ReadOnlyArchiveInterface *interface); QString destinationDirectory() const; ExtractionOptions extractionOptions() const; public slots: virtual void doWork() Q_DECL_OVERRIDE; private: QVector m_entries; QString m_destinationDir; ExtractionOptions m_options; }; /** * Abstract base class for jobs that extract a single file to a temporary dir. * It's not possible to pass extraction options and paths will be always preserved. * The only option that the job needs to know is whether the file is password protected. */ class KERFUFFLE_EXPORT TempExtractJob : public Job { Q_OBJECT public: TempExtractJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface); /** * @return The absolute path of the extracted file. * The path is validated in order to prevent directory traversal attacks. */ QString validatedFilePath() const; ExtractionOptions extractionOptions() const; /** * @return The temporary dir used for the extraction. * It is safe to delete this pointer in order to remove the directory. */ QTemporaryDir *tempDir() const; public slots: virtual void doWork() Q_DECL_OVERRIDE; private: QString extractionDir() const; Archive::Entry *m_entry; QTemporaryDir *m_tmpExtractDir; bool m_passwordProtectedHint; }; /** * This TempExtractJob can be used to preview a file. * The temporary extraction directory will be deleted upon job's completion. */ class KERFUFFLE_EXPORT PreviewJob : public TempExtractJob { Q_OBJECT public: PreviewJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface); }; /** * This TempExtractJob can be used to open a file in its dedicated application. * For this reason, the temporary extraction directory will NOT be deleted upon job's completion. */ class KERFUFFLE_EXPORT OpenJob : public TempExtractJob { Q_OBJECT public: OpenJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface); }; class KERFUFFLE_EXPORT OpenWithJob : public OpenJob { Q_OBJECT public: OpenWithJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface); }; class KERFUFFLE_EXPORT AddJob : public Job { Q_OBJECT public: AddJob(const QVector &files, const Archive::Entry *destination, const CompressionOptions& options, ReadWriteArchiveInterface *interface); public slots: virtual void doWork() Q_DECL_OVERRIDE; protected slots: virtual void onFinished(bool result) Q_DECL_OVERRIDE; private: QString m_oldWorkingDir; const QVector m_entries; const Archive::Entry *m_destination; CompressionOptions m_options; }; /** * This MoveJob can be used to rename or move entries within the archive. * @see Archive::moveFiles for more details. */ class KERFUFFLE_EXPORT MoveJob : public Job { Q_OBJECT public: MoveJob(const QVector &files, Archive::Entry *destination, const CompressionOptions& options, ReadWriteArchiveInterface *interface); public slots: virtual void doWork() Q_DECL_OVERRIDE; protected slots: virtual void onFinished(bool result) Q_DECL_OVERRIDE; private: int m_finishedSignalsCount; const QVector m_entries; Archive::Entry *m_destination; CompressionOptions m_options; }; /** * This CopyJob can be used to copy entries within the archive. * @see Archive::copyFiles for more details. */ class KERFUFFLE_EXPORT CopyJob : public Job { Q_OBJECT public: CopyJob(const QVector &entries, Archive::Entry *destination, const CompressionOptions& options, ReadWriteArchiveInterface *interface); public slots: virtual void doWork() Q_DECL_OVERRIDE; protected slots: virtual void onFinished(bool result) Q_DECL_OVERRIDE; private: int m_finishedSignalsCount; const QVector m_entries; Archive::Entry *m_destination; CompressionOptions m_options; }; class KERFUFFLE_EXPORT DeleteJob : public Job { Q_OBJECT public: DeleteJob(const QVector &files, ReadWriteArchiveInterface *interface); public slots: virtual void doWork() Q_DECL_OVERRIDE; private: QVector m_entries; }; class KERFUFFLE_EXPORT CommentJob : public Job { Q_OBJECT public: CommentJob(const QString& comment, ReadWriteArchiveInterface *interface); public slots: virtual void doWork() Q_DECL_OVERRIDE; private: QString m_comment; }; class KERFUFFLE_EXPORT TestJob : public Job { Q_OBJECT public: explicit TestJob(ReadOnlyArchiveInterface *interface); bool testSucceeded(); public slots: virtual void doWork() Q_DECL_OVERRIDE; private slots: virtual void onTestSuccess(); private: bool m_testSuccess; }; } // namespace Kerfuffle #endif // JOBS_H diff --git a/plugins/clirarplugin/cliplugin.cpp b/plugins/clirarplugin/cliplugin.cpp index d0eb730e..311edb5d 100644 --- a/plugins/clirarplugin/cliplugin.cpp +++ b/plugins/clirarplugin/cliplugin.cpp @@ -1,579 +1,584 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2010-2011,2014 Raphael Kubo da Costa * Copyright (C) 2015-2016 Ragnar Thomsen * Copyright (c) 2016 Vladyslav Batyrenko * * 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; either version 2 * of the License, or (at your option) any later version. * * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "cliplugin.h" #include "ark_debug.h" #include "kerfuffle/archiveentry.h" #include #include #include using namespace Kerfuffle; K_PLUGIN_FACTORY_WITH_JSON(CliPluginFactory, "kerfuffle_clirar.json", registerPlugin();) CliPlugin::CliPlugin(QObject *parent, const QVariantList& args) : CliInterface(parent, args) , m_parseState(ParseStateTitle) , m_isUnrar5(false) , m_isPasswordProtected(false) , m_isSolid(false) , m_isRAR5(false) , m_remainingIgnoreLines(1) //The first line of UNRAR output is empty. , m_linesComment(0) { qCDebug(ARK) << "Loaded cli_rar plugin"; // Empty lines are needed for parsing output of unrar. setListEmptyLines(true); setupCliProperties(); } CliPlugin::~CliPlugin() { } void CliPlugin::resetParsing() { m_parseState = ParseStateTitle; m_remainingIgnoreLines = 1; m_unrarVersion.clear(); m_comment.clear(); m_numberOfVolumes = 0; } void CliPlugin::setupCliProperties() { qCDebug(ARK) << "Setting up parameters..."; m_cliProps->setProperty("captureProgress", true); m_cliProps->setProperty("addProgram", QStringLiteral("rar")); m_cliProps->setProperty("addSwitch", QStringList({QStringLiteral("a")})); m_cliProps->setProperty("deleteProgram", QStringLiteral("rar")); m_cliProps->setProperty("deleteSwitch", QStringLiteral("d")); m_cliProps->setProperty("extractProgram", QStringLiteral("unrar")); m_cliProps->setProperty("extractSwitch", QStringList{QStringLiteral("x"), QStringLiteral("-kb"), QStringLiteral("-p-")}); m_cliProps->setProperty("extractSwitchNoPreserve", QStringList{QStringLiteral("e"), QStringLiteral("-kb"), QStringLiteral("-p-")}); m_cliProps->setProperty("listProgram", QStringLiteral("unrar")); m_cliProps->setProperty("listSwitch", QStringList{QStringLiteral("vt"), QStringLiteral("-v")}); m_cliProps->setProperty("moveProgram", QStringLiteral("rar")); m_cliProps->setProperty("moveSwitch", QStringLiteral("rn")); m_cliProps->setProperty("testProgram", QStringLiteral("unrar")); m_cliProps->setProperty("testSwitch", QStringLiteral("t")); m_cliProps->setProperty("commentSwitch", QStringList{QStringLiteral("c"), QStringLiteral("-z$CommentFile")}); m_cliProps->setProperty("passwordSwitch", QStringList{QStringLiteral("-p$Password")}); m_cliProps->setProperty("passwordSwitchHeaderEnc", QStringList{QStringLiteral("-hp$Password")}); m_cliProps->setProperty("compressionLevelSwitch", QStringLiteral("-m$CompressionLevel")); m_cliProps->setProperty("compressionMethodSwitch", QHash{{QStringLiteral("application/vnd.rar"), QStringLiteral("-ma$CompressionMethod")}, {QStringLiteral("application/x-rar"), QStringLiteral("-ma$CompressionMethod")}}); m_cliProps->setProperty("multiVolumeSwitch", QStringLiteral("-v$VolumeSizek")); m_cliProps->setProperty("passwordPromptPatterns", QStringList{QStringLiteral("Enter password \\(will not be echoed\\) for")}); m_cliProps->setProperty("wrongPasswordPatterns", QStringList{QStringLiteral("password incorrect"), QStringLiteral("wrong password")}); m_cliProps->setProperty("testPassedPatterns", QStringList{QStringLiteral("^All OK$")}); m_cliProps->setProperty("fileExistsPatterns", QStringList{QStringLiteral("^\\[Y\\]es, \\[N\\]o, \\[A\\]ll, n\\[E\\]ver, \\[R\\]ename, \\[Q\\]uit $")}); m_cliProps->setProperty("fileExistsFileName", QStringList{QStringLiteral("^(.+) already exists. Overwrite it"), // unrar 3 & 4 QStringLiteral("^Would you like to replace the existing file (.+)$")}); // unrar 5 m_cliProps->setProperty("fileExistsInput", QStringList{QStringLiteral("Y"), //Overwrite QStringLiteral("N"), //Skip QStringLiteral("A"), //Overwrite all QStringLiteral("E"), //Autoskip QStringLiteral("Q")}); //Cancel m_cliProps->setProperty("corruptArchivePatterns", QStringList{QStringLiteral("Unexpected end of archive"), QStringLiteral("the file header is corrupt")}); m_cliProps->setProperty("diskFullPatterns", QStringList{QStringLiteral("No space left on device")}); // rar will sometimes create multi-volume archives where first volume is // called name.part1.rar and other times name.part01.rar. m_cliProps->setProperty("multiVolumeSuffix", QStringList{QStringLiteral("part01.$Suffix"), QStringLiteral("part1.$Suffix")}); } bool CliPlugin::readListLine(const QString &line) { // Ignore number of lines corresponding to m_remainingIgnoreLines. if (m_remainingIgnoreLines > 0) { --m_remainingIgnoreLines; return true; } // Parse the title line, which contains the version of unrar. if (m_parseState == ParseStateTitle) { QRegularExpression rxVersionLine(QStringLiteral("^UNRAR (\\d+\\.\\d+)( beta \\d)? .*$")); QRegularExpressionMatch matchVersion = rxVersionLine.match(line); if (matchVersion.hasMatch()) { m_parseState = ParseStateComment; m_unrarVersion = matchVersion.captured(1); qCDebug(ARK) << "UNRAR version" << m_unrarVersion << "detected"; if (m_unrarVersion.toFloat() >= 5) { m_isUnrar5 = true; qCDebug(ARK) << "Using UNRAR 5 parser"; } else { qCDebug(ARK) << "Using UNRAR 4 parser"; } } else { // If the second line doesn't contain an UNRAR title, something // is wrong. qCCritical(ARK) << "Failed to detect UNRAR output."; return false; } // Or see what version of unrar we are dealing with and call specific // handler functions. } else if (m_isUnrar5) { return handleUnrar5Line(line); } else { return handleUnrar4Line(line); } return true; } bool CliPlugin::handleUnrar5Line(const QString &line) { const QRegularExpression rxVolume(QStringLiteral("Cannot find volume ")); if (rxVolume.match(line).hasMatch()) { emit error(i18n("Failed to find all archive volumes.")); return false; } // Parses the comment field. if (m_parseState == ParseStateComment) { // RegExp matching end of comment field. // FIXME: Comment itself could also contain the Archive path string here. QRegularExpression rxCommentEnd(QStringLiteral("^Archive: .+$")); if (rxCommentEnd.match(line).hasMatch()) { m_parseState = ParseStateHeader; m_comment = m_comment.trimmed(); m_linesComment = m_comment.count(QLatin1Char('\n')) + 1; if (!m_comment.isEmpty()) { qCDebug(ARK) << "Found a comment with" << m_linesComment << "lines"; } } else { m_comment.append(line + QLatin1Char('\n')); } return true; } // Parses the header, which is whatever is between the comment field // and the entries. else if (m_parseState == ParseStateHeader) { // "Details: " indicates end of header. if (line.startsWith(QStringLiteral("Details: "))) { ignoreLines(1, ParseStateEntryDetails); if (line.contains(QLatin1String("volume"))) { m_numberOfVolumes++; if (!isMultiVolume()) { setMultiVolume(true); qCDebug(ARK) << "Multi-volume archive detected"; } } if (line.contains(QLatin1String("solid")) && !m_isSolid) { m_isSolid = true; qCDebug(ARK) << "Solid archive detected"; } if (line.contains(QLatin1String("RAR 4"))) { emit compressionMethodFound(QStringLiteral("RAR4")); } else if (line.contains(QLatin1String("RAR 5"))) { emit compressionMethodFound(QStringLiteral("RAR5")); m_isRAR5 = true; } } return true; } // Parses the entry details for each entry. else if (m_parseState == ParseStateEntryDetails) { // For multi-volume archives there is a header between the entries in // each volume. if (line.startsWith(QLatin1String("Archive: "))) { m_parseState = ParseStateHeader; return true; // Empty line indicates end of entry. } else if (line.trimmed().isEmpty() && !m_unrar5Details.isEmpty()) { handleUnrar5Entry(); } else { // All detail lines should contain a colon. if (!line.contains(QLatin1Char(':'))) { qCWarning(ARK) << "Unrecognized line:" << line; return true; } // The details are on separate lines, so we store them in the QHash // m_unrar5Details. m_unrar5Details.insert(line.section(QLatin1Char(':'), 0, 0).trimmed().toLower(), line.section(QLatin1Char(':'), 1).trimmed()); } return true; } return true; } void CliPlugin::handleUnrar5Entry() { Archive::Entry *e = new Archive::Entry(this); QString compressionRatio = m_unrar5Details.value(QStringLiteral("ratio")); compressionRatio.chop(1); // Remove the '%' e->setProperty("ratio", compressionRatio); QString time = m_unrar5Details.value(QStringLiteral("mtime")); QDateTime ts = QDateTime::fromString(time, QStringLiteral("yyyy-MM-dd HH:mm:ss,zzz")); e->setProperty("timestamp", ts); bool isDirectory = (m_unrar5Details.value(QStringLiteral("type")) == QLatin1String("Directory")); e->setProperty("isDirectory", isDirectory); if (isDirectory && !m_unrar5Details.value(QStringLiteral("name")).endsWith(QLatin1Char('/'))) { m_unrar5Details[QStringLiteral("name")] += QLatin1Char('/'); } QString compression = m_unrar5Details.value(QStringLiteral("compression")); int optionPos = compression.indexOf(QLatin1Char('-')); if (optionPos != -1) { e->setProperty("method", compression.mid(optionPos)); e->setProperty("version", compression.left(optionPos).trimmed()); } else { // No method specified. e->setProperty("method", QStringLiteral("")); e->setProperty("version", compression); } m_isPasswordProtected = m_unrar5Details.value(QStringLiteral("flags")).contains(QStringLiteral("encrypted")); e->setProperty("isPasswordProtected", m_isPasswordProtected); if (m_isPasswordProtected) { m_isRAR5 ? emit encryptionMethodFound(QStringLiteral("AES256")) : emit encryptionMethodFound(QStringLiteral("AES128")); } e->setProperty("fullPath", m_unrar5Details.value(QStringLiteral("name"))); e->setProperty("size", m_unrar5Details.value(QStringLiteral("size"))); e->setProperty("compressedSize", m_unrar5Details.value(QStringLiteral("packed size"))); e->setProperty("permissions", m_unrar5Details.value(QStringLiteral("attributes"))); e->setProperty("CRC", m_unrar5Details.value(QStringLiteral("crc32"))); if (e->property("permissions").toString().startsWith(QLatin1Char('l'))) { e->setProperty("link", m_unrar5Details.value(QStringLiteral("target"))); } m_unrar5Details.clear(); emit entry(e); } bool CliPlugin::handleUnrar4Line(const QString &line) { const QRegularExpression rxVolume(QStringLiteral("Cannot find volume ")); if (rxVolume.match(line).hasMatch()) { emit error(i18n("Failed to find all archive volumes.")); return false; } // Parses the comment field. if (m_parseState == ParseStateComment) { // RegExp matching end of comment field. // FIXME: Comment itself could also contain the Archive path string here. QRegularExpression rxCommentEnd(QStringLiteral("^(Solid archive|Archive|Volume) .+$")); // unrar 4 outputs the following string when opening v5 RAR archives. if (line == QLatin1String("Unsupported archive format. Please update RAR to a newer version.")) { emit error(i18n("Your unrar executable is version %1, which is too old to handle this archive. Please update to a more recent version.", m_unrarVersion)); return false; } // unrar 3 reports a non-RAR archive when opening v5 RAR archives. if (line.endsWith(QLatin1String(" is not RAR archive"))) { emit error(i18n("Unrar reported a non-RAR archive. The installed unrar version (%1) is old. Try updating your unrar.", m_unrarVersion)); return false; } // If we reach this point, then we can be sure that it's not a RAR5 // archive, so assume RAR4. emit compressionMethodFound(QStringLiteral("RAR4")); if (rxCommentEnd.match(line).hasMatch()) { if (line.startsWith(QLatin1String("Volume "))) { m_numberOfVolumes++; if (!isMultiVolume()) { setMultiVolume(true); qCDebug(ARK) << "Multi-volume archive detected"; } } if (line.startsWith(QLatin1String("Solid archive")) && !m_isSolid) { m_isSolid = true; qCDebug(ARK) << "Solid archive detected"; } m_parseState = ParseStateHeader; m_comment = m_comment.trimmed(); m_linesComment = m_comment.count(QLatin1Char('\n')) + 1; if (!m_comment.isEmpty()) { qCDebug(ARK) << "Found a comment with" << m_linesComment << "lines"; } } else { m_comment.append(line + QLatin1Char('\n')); } return true; } // Parses the header, which is whatever is between the comment field // and the entries. else if (m_parseState == ParseStateHeader) { // Horizontal line indicates end of header. if (line.startsWith(QStringLiteral("--------------------"))) { m_parseState = ParseStateEntryFileName; } else if (line.startsWith(QLatin1String("Volume "))) { m_numberOfVolumes++; } return true; } // Parses the entry name, which is on the first line of each entry. else if (m_parseState == ParseStateEntryFileName) { // Ignore empty lines. if (line.trimmed().isEmpty()) { return true; } // Three types of subHeaders can be displayed for unrar 3 and 4. // STM has 4 lines, RR has 3, and CMT has lines corresponding to // length of comment field +3. We ignore the subheaders. QRegularExpression rxSubHeader(QStringLiteral("^Data header type: (CMT|STM|RR)$")); QRegularExpressionMatch matchSubHeader = rxSubHeader.match(line); if (matchSubHeader.hasMatch()) { qCDebug(ARK) << "SubHeader of type" << matchSubHeader.captured(1) << "found"; if (matchSubHeader.captured(1) == QLatin1String("STM")) { ignoreLines(4, ParseStateEntryFileName); } else if (matchSubHeader.captured(1) == QLatin1String("CMT")) { ignoreLines(m_linesComment + 3, ParseStateEntryFileName); } else if (matchSubHeader.captured(1) == QLatin1String("RR")) { ignoreLines(3, ParseStateEntryFileName); } return true; } // The entries list ends with a horizontal line, followed by a // single summary line or, for multi-volume archives, another header. if (line.startsWith(QStringLiteral("-----------------"))) { m_parseState = ParseStateHeader; return true; // Encrypted files are marked with an asterisk. } else if (line.startsWith(QLatin1Char('*'))) { m_isPasswordProtected = true; m_unrar4Details.append(QString(line.trimmed()).remove(0, 1)); //Remove the asterisk emit encryptionMethodFound(QStringLiteral("AES128")); // Entry names always start at the second position, so a line not // starting with a space is not an entry name. } else if (!line.startsWith(QLatin1Char(' '))) { qCWarning(ARK) << "Unrecognized line:" << line; return true; // If we reach this, then we can assume the line is an entry name, so // save it, and move on to the rest of the entry details. } else { m_unrar4Details.append(line.trimmed()); } m_parseState = ParseStateEntryDetails; return true; } // Parses the remainder of the entry details for each entry. else if (m_parseState == ParseStateEntryDetails) { // If the line following an entry name is empty, we did something // wrong. Q_ASSERT(!line.trimmed().isEmpty()); // If we reach a horizontal line, then the previous line was not an // entry name, so go back to header. if (line.startsWith(QStringLiteral("-----------------"))) { m_parseState = ParseStateHeader; return true; } // In unrar 3 and 4 the details are on a single line, so we // pass a QStringList containing the details. We need to store // it due to symlinks (see below). m_unrar4Details.append(line.split(QLatin1Char(' '), QString::SkipEmptyParts)); // The details line contains 9 fields, so m_unrar4Details // should now contain 9 + the filename = 10 strings. If not, this is // not an archive entry. if (m_unrar4Details.size() != 10) { m_parseState = ParseStateHeader; return true; } // When unrar 3 and 4 list a symlink, they output an extra line // containing the link target. The extra line is output after // the line we ignore, so we first need to ignore one line. if (m_unrar4Details.at(6).startsWith(QLatin1Char('l'))) { ignoreLines(1, ParseStateLinkTarget); return true; } else { handleUnrar4Entry(); } // Unrar 3 & 4 show a third line for each entry, which contains // three details: Host OS, Solid, and Old. We can ignore this // line. ignoreLines(1, ParseStateEntryFileName); return true; } // Parses a symlink target. else if (m_parseState == ParseStateLinkTarget) { m_unrar4Details.append(QString(line).remove(QStringLiteral("-->")).trimmed()); handleUnrar4Entry(); m_parseState = ParseStateEntryFileName; return true; } return true; } void CliPlugin::handleUnrar4Entry() { Archive::Entry *e = new Archive::Entry(this); QDateTime ts = QDateTime::fromString(QString(m_unrar4Details.at(4) + QLatin1Char(' ') + m_unrar4Details.at(5)), QStringLiteral("dd-MM-yy hh:mm")); // Unrar 3 & 4 output dates with a 2-digit year but QDateTime takes it as // 19??. Let's take 1950 as cut-off; similar to KDateTime. if (ts.date().year() < 1950) { ts = ts.addYears(100); } e->setProperty("timestamp", ts); bool isDirectory = ((m_unrar4Details.at(6).at(0) == QLatin1Char('d')) || (m_unrar4Details.at(6).at(1) == QLatin1Char('D'))); e->setProperty("isDirectory", isDirectory); if (isDirectory && !m_unrar4Details.at(0).endsWith(QLatin1Char('/'))) { m_unrar4Details[0] += QLatin1Char('/'); } // Unrar reports the ratio as ((compressed size * 100) / size); // we consider ratio as (100 * ((size - compressed size) / size)). // If the archive is a multivolume archive, a string indicating // whether the archive's position in the volume is displayed // instead of the compression ratio. QString compressionRatio = m_unrar4Details.at(3); if ((compressionRatio == QStringLiteral("<--")) || (compressionRatio == QStringLiteral("<->")) || (compressionRatio == QStringLiteral("-->"))) { compressionRatio = QLatin1Char('0'); } else { compressionRatio.chop(1); // Remove the '%' } e->setProperty("ratio", compressionRatio); // TODO: // - Permissions differ depending on the system the entry was added // to the archive. e->setProperty("fullPath", m_unrar4Details.at(0)); e->setProperty("size", m_unrar4Details.at(1)); e->setProperty("compressedSize", m_unrar4Details.at(2)); e->setProperty("permissions", m_unrar4Details.at(6)); e->setProperty("CRC", m_unrar4Details.at(7)); e->setProperty("method", m_unrar4Details.at(8)); e->setProperty("version", m_unrar4Details.at(9)); e->setProperty("isPasswordProtected", m_isPasswordProtected); if (e->property("permissions").toString().startsWith(QLatin1Char('l'))) { e->setProperty("link", m_unrar4Details.at(10)); } m_unrar4Details.clear(); emit entry(e); } bool CliPlugin::readExtractLine(const QString &line) { const QRegularExpression rxCRC(QStringLiteral("CRC failed")); if (rxCRC.match(line).hasMatch()) { emit error(i18n("One or more wrong checksums")); return false; } const QRegularExpression rxVolume(QStringLiteral("Cannot find volume ")); if (rxVolume.match(line).hasMatch()) { emit error(i18n("Failed to find all archive volumes.")); return false; } return true; } +bool CliPlugin::hasBatchExtractionProgress() const +{ + return true; +} + void CliPlugin::ignoreLines(int lines, ParseState nextState) { m_remainingIgnoreLines = lines; m_parseState = nextState; } #include "cliplugin.moc" diff --git a/plugins/clirarplugin/cliplugin.h b/plugins/clirarplugin/cliplugin.h index 59d5e52f..f597f8df 100644 --- a/plugins/clirarplugin/cliplugin.h +++ b/plugins/clirarplugin/cliplugin.h @@ -1,74 +1,75 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2010 Raphael Kubo da Costa * Copyright (C) 2015-2016 Ragnar Thomsen * Copyright (c) 2016 Vladyslav Batyrenko * * 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; either version 2 * of the License, or (at your option) any later version. * * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #ifndef CLIPLUGIN_H #define CLIPLUGIN_H #include "kerfuffle/cliinterface.h" class CliPlugin : public Kerfuffle::CliInterface { Q_OBJECT public: explicit CliPlugin(QObject *parent, const QVariantList &args); virtual ~CliPlugin(); virtual void resetParsing() Q_DECL_OVERRIDE; virtual bool readListLine(const QString &line) Q_DECL_OVERRIDE; virtual bool readExtractLine(const QString &line) Q_DECL_OVERRIDE; + virtual bool hasBatchExtractionProgress() const Q_DECL_OVERRIDE; private: enum ParseState { ParseStateTitle = 0, ParseStateComment, ParseStateHeader, ParseStateEntryFileName, ParseStateEntryDetails, ParseStateLinkTarget } m_parseState; void setupCliProperties(); bool handleUnrar5Line(const QString &line); void handleUnrar5Entry(); bool handleUnrar4Line(const QString &line); void handleUnrar4Entry(); void ignoreLines(int lines, ParseState nextState); QStringList m_unrar4Details; QHash m_unrar5Details; QString m_unrarVersion; bool m_isUnrar5; bool m_isPasswordProtected; bool m_isSolid; bool m_isRAR5; int m_remainingIgnoreLines; int m_linesComment; }; #endif // CLIPLUGIN_H diff --git a/plugins/libarchive/libarchiveplugin.cpp b/plugins/libarchive/libarchiveplugin.cpp index 1d889e59..5894e2f6 100644 --- a/plugins/libarchive/libarchiveplugin.cpp +++ b/plugins/libarchive/libarchiveplugin.cpp @@ -1,567 +1,572 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2010 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "libarchiveplugin.h" #include "kerfuffle/queries.h" #include #include #include LibarchivePlugin::LibarchivePlugin(QObject *parent, const QVariantList &args) : ReadWriteArchiveInterface(parent, args) , m_archiveReadDisk(archive_read_disk_new()) , m_cachedArchiveEntryCount(0) , m_emitNoEntries(false) , m_extractedFilesSize(0) { qCDebug(ARK) << "Initializing libarchive plugin"; archive_read_disk_set_standard_lookup(m_archiveReadDisk.data()); } LibarchivePlugin::~LibarchivePlugin() { foreach (const auto e, m_emittedEntries) { // Entries might be passed to pending slots, so we just schedule their deletion. e->deleteLater(); } } bool LibarchivePlugin::list() { qCDebug(ARK) << "Listing archive contents"; if (!initializeReader()) { return false; } qDebug(ARK) << "Detected compression filter:" << archive_filter_name(m_archiveReader.data(), 0); QString compMethod = convertCompressionName(QString::fromUtf8(archive_filter_name(m_archiveReader.data(), 0))); if (!compMethod.isEmpty()) { emit compressionMethodFound(compMethod); } m_cachedArchiveEntryCount = 0; m_extractedFilesSize = 0; m_numberOfEntries = 0; qulonglong compressedArchiveSize = QFileInfo(filename()).size(); struct archive_entry *aentry; int result = ARCHIVE_RETRY; bool firstEntry = true; while (!QThread::currentThread()->isInterruptionRequested() && (result = archive_read_next_header(m_archiveReader.data(), &aentry)) == ARCHIVE_OK) { if (firstEntry) { qDebug(ARK) << "Detected format for first entry:" << archive_format_name(m_archiveReader.data()); firstEntry = false; } if (!m_emitNoEntries) { emitEntryFromArchiveEntry(aentry); } m_extractedFilesSize += (qlonglong)archive_entry_size(aentry); emit progress(float(archive_filter_bytes(m_archiveReader.data(), -1))/float(compressedArchiveSize)); m_cachedArchiveEntryCount++; archive_read_data_skip(m_archiveReader.data()); } if (result != ARCHIVE_EOF) { qCWarning(ARK) << "Could not read until the end of the archive:" << QLatin1String(archive_error_string(m_archiveReader.data())); return false; } return archive_read_close(m_archiveReader.data()) == ARCHIVE_OK; } bool LibarchivePlugin::addFiles(const QVector &files, const Archive::Entry *destination, const CompressionOptions &options, uint numberOfEntriesToAdd) { Q_UNUSED(files) Q_UNUSED(destination) Q_UNUSED(options) Q_UNUSED(numberOfEntriesToAdd) return false; } bool LibarchivePlugin::moveFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) { Q_UNUSED(files) Q_UNUSED(destination) Q_UNUSED(options) return false; } bool LibarchivePlugin::copyFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) { Q_UNUSED(files) Q_UNUSED(destination) Q_UNUSED(options) return false; } bool LibarchivePlugin::deleteFiles(const QVector &files) { Q_UNUSED(files) return false; } bool LibarchivePlugin::addComment(const QString &comment) { Q_UNUSED(comment) return false; } bool LibarchivePlugin::testArchive() { return false; } +bool LibarchivePlugin::hasBatchExtractionProgress() const +{ + return true; +} + bool LibarchivePlugin::doKill() { return true; } bool LibarchivePlugin::extractFiles(const QVector &files, const QString &destinationDirectory, const ExtractionOptions &options) { qCDebug(ARK) << "Changing current directory to " << destinationDirectory; QDir::setCurrent(destinationDirectory); const bool extractAll = files.isEmpty(); const bool preservePaths = options.preservePaths(); const bool removeRootNode = options.isDragAndDropEnabled(); // To avoid traversing the entire archive when extracting a limited set of // entries, we maintain a list of remaining entries and stop when it's // empty. QStringList fullPaths = entryFullPaths(files); QStringList remainingFiles = entryFullPaths(files); if (!initializeReader()) { return false; } ArchiveWrite writer(archive_write_disk_new()); if (!writer.data()) { return false; } archive_write_disk_set_options(writer.data(), extractionFlags()); int entryNr = 0; int totalCount = 0; if (extractAll) { if (!m_cachedArchiveEntryCount) { emit progress(0); //TODO: once information progress has been implemented, send //feedback here that the archive is being read qCDebug(ARK) << "For getting progress information, the archive will be listed once"; m_emitNoEntries = true; list(); m_emitNoEntries = false; } totalCount = m_cachedArchiveEntryCount; } else { totalCount = files.size(); } qCDebug(ARK) << "Going to extract" << totalCount << "entries"; // Initialize variables. bool overwriteAll = false; // Whether to overwrite all files bool skipAll = false; // Whether to skip all files bool dontPromptErrors = false; // Whether to prompt for errors m_currentExtractedFilesSize = 0; int no_entries = 0; struct archive_entry *entry; QString fileBeingRenamed; // Iterate through all entries in archive. while (!QThread::currentThread()->isInterruptionRequested() && (archive_read_next_header(m_archiveReader.data(), &entry) == ARCHIVE_OK)) { if (!extractAll && remainingFiles.isEmpty()) { break; } fileBeingRenamed.clear(); int index = -1; // Retry with renamed entry, fire an overwrite query again // if the new entry also exists. retry: const bool entryIsDir = S_ISDIR(archive_entry_mode(entry)); // Skip directories if not preserving paths. if (!preservePaths && entryIsDir) { archive_read_data_skip(m_archiveReader.data()); continue; } // entryName is the name inside the archive, full path QString entryName = QDir::fromNativeSeparators(QFile::decodeName(archive_entry_pathname(entry))); // Some archive types e.g. AppImage prepend all entries with "./" so remove this part. if (entryName.startsWith(QLatin1String("./"))) { entryName.remove(0, 2); } // Static libraries (*.a) contain the two entries "/" and "//". // We just skip these to allow extracting this archive type. if (entryName == QLatin1String("/") || entryName == QLatin1String("//")) { archive_read_data_skip(m_archiveReader.data()); continue; } // For now we just can't handle absolute filenames in a tar archive. // TODO: find out what to do here!! if (entryName.startsWith(QLatin1Char( '/' ))) { emit error(i18n("This archive contains archive entries with absolute paths, " "which are not supported by Ark.")); return false; } // Should the entry be extracted? if (extractAll || remainingFiles.contains(entryName) || entryName == fileBeingRenamed) { // Find the index of entry. if (entryName != fileBeingRenamed) { index = fullPaths.indexOf(entryName); } if (!extractAll && index == -1) { // If entry is not found in files, skip entry. continue; } // entryFI is the fileinfo pointing to where the file will be // written from the archive. QFileInfo entryFI(entryName); //qCDebug(ARK) << "setting path to " << archive_entry_pathname( entry ); const QString fileWithoutPath(entryFI.fileName()); // If we DON'T preserve paths, we cut the path and set the entryFI // fileinfo to the one without the path. if (!preservePaths) { // Empty filenames (ie dirs) should have been skipped already, // so asserting. Q_ASSERT(!fileWithoutPath.isEmpty()); archive_entry_copy_pathname(entry, QFile::encodeName(fileWithoutPath).constData()); entryFI = QFileInfo(fileWithoutPath); // OR, if the file has a rootNode attached, remove it from file path. } else if (!extractAll && removeRootNode && entryName != fileBeingRenamed) { const QString &rootNode = files.at(index)->rootNode; if (!rootNode.isEmpty()) { const QString truncatedFilename(entryName.remove(entryName.indexOf(rootNode), rootNode.size())); archive_entry_copy_pathname(entry, QFile::encodeName(truncatedFilename).constData()); entryFI = QFileInfo(truncatedFilename); } } // Check if the file about to be written already exists. if (!entryIsDir && entryFI.exists()) { if (skipAll) { archive_read_data_skip(m_archiveReader.data()); archive_entry_clear(entry); continue; } else if (!overwriteAll && !skipAll) { Kerfuffle::OverwriteQuery query(entryName); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { archive_read_data_skip(m_archiveReader.data()); archive_entry_clear(entry); break; } else if (query.responseSkip()) { archive_read_data_skip(m_archiveReader.data()); archive_entry_clear(entry); continue; } else if (query.responseAutoSkip()) { archive_read_data_skip(m_archiveReader.data()); archive_entry_clear(entry); skipAll = true; continue; } else if (query.responseRename()) { const QString newName(query.newFilename()); fileBeingRenamed = newName; archive_entry_copy_pathname(entry, QFile::encodeName(newName).constData()); goto retry; } else if (query.responseOverwriteAll()) { overwriteAll = true; } } } // If there is an already existing directory. if (entryIsDir && entryFI.exists()) { if (entryFI.isWritable()) { qCWarning(ARK) << "Warning, existing, but writable dir"; } else { qCWarning(ARK) << "Warning, existing, but non-writable dir. skipping"; archive_entry_clear(entry); archive_read_data_skip(m_archiveReader.data()); continue; } } // Write the entry header and check return value. const int returnCode = archive_write_header(writer.data(), entry); switch (returnCode) { case ARCHIVE_OK: // If the whole archive is extracted and the total filesize is // available, we use partial progress. copyData(entryName, m_archiveReader.data(), writer.data(), (extractAll && m_extractedFilesSize)); break; case ARCHIVE_FAILED: qCCritical(ARK) << "archive_write_header() has returned" << returnCode << "with errno" << archive_errno(writer.data()); // If they user previously decided to ignore future errors, // don't bother prompting again. if (!dontPromptErrors) { // Ask the user if he wants to continue extraction despite an error for this entry. Kerfuffle::ContinueExtractionQuery query(QLatin1String(archive_error_string(writer.data())), entryName); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); return false; } dontPromptErrors = query.dontAskAgain(); } break; case ARCHIVE_FATAL: qCCritical(ARK) << "archive_write_header() has returned" << returnCode << "with errno" << archive_errno(writer.data()); emit error(i18nc("@info", "Fatal error, extraction aborted.")); return false; default: qCDebug(ARK) << "archive_write_header() returned" << returnCode << "which will be ignored."; break; } // If we only partially extract the archive and the number of // archive entries is available we use a simple progress based on // number of items extracted. if (!extractAll && m_cachedArchiveEntryCount) { ++entryNr; emit progress(float(entryNr) / totalCount); } no_entries++; remainingFiles.removeOne(entryName); } else { // Archive entry not among selected files, skip it. archive_read_data_skip(m_archiveReader.data()); } } // While entries left to read in archive. qCDebug(ARK) << "Extracted" << no_entries << "entries"; return archive_read_close(m_archiveReader.data()) == ARCHIVE_OK; } bool LibarchivePlugin::initializeReader() { m_archiveReader.reset(archive_read_new()); if (!(m_archiveReader.data())) { emit error(i18n("The archive reader could not be initialized.")); return false; } if (archive_read_support_filter_all(m_archiveReader.data()) != ARCHIVE_OK) { return false; } if (archive_read_support_format_all(m_archiveReader.data()) != ARCHIVE_OK) { return false; } if (archive_read_open_filename(m_archiveReader.data(), QFile::encodeName(filename()), 10240) != ARCHIVE_OK) { qCWarning(ARK) << "Could not open the archive:" << archive_error_string(m_archiveReader.data()); emit error(i18nc("@info", "Archive corrupted or insufficient permissions.")); return false; } return true; } void LibarchivePlugin::emitEntryFromArchiveEntry(struct archive_entry *aentry) { auto e = new Archive::Entry(); #ifdef Q_OS_WIN e->setProperty("fullPath", QDir::fromNativeSeparators(QString::fromUtf16((ushort*)archive_entry_pathname_w(aentry)))); #else e->setProperty("fullPath", QDir::fromNativeSeparators(QString::fromWCharArray(archive_entry_pathname_w(aentry)))); #endif const QString owner = QString::fromLatin1(archive_entry_uname(aentry)); if (!owner.isEmpty()) { e->setProperty("owner", owner); } const QString group = QString::fromLatin1(archive_entry_gname(aentry)); if (!group.isEmpty()) { e->setProperty("group", group); } e->compressedSizeIsSet = false; e->setProperty("size", (qlonglong)archive_entry_size(aentry)); e->setProperty("isDirectory", S_ISDIR(archive_entry_mode(aentry))); if (archive_entry_symlink(aentry)) { e->setProperty("link", QLatin1String( archive_entry_symlink(aentry) )); } e->setProperty("timestamp", QDateTime::fromTime_t(archive_entry_mtime(aentry))); emit entry(e); m_emittedEntries << e; } int LibarchivePlugin::extractionFlags() const { int result = ARCHIVE_EXTRACT_TIME; result |= ARCHIVE_EXTRACT_SECURE_NODOTDOT; // TODO: Don't use arksettings here /*if ( ArkSettings::preservePerms() ) { result &= ARCHIVE_EXTRACT_PERM; } if ( !ArkSettings::extractOverwrite() ) { result &= ARCHIVE_EXTRACT_NO_OVERWRITE; }*/ return result; } void LibarchivePlugin::copyData(const QString& filename, struct archive *dest, bool partialprogress) { char buff[10240]; ssize_t readBytes; QFile file(filename); if (!file.open(QIODevice::ReadOnly)) { return; } readBytes = file.read(buff, sizeof(buff)); while (readBytes > 0) { archive_write_data(dest, buff, readBytes); if (archive_errno(dest) != ARCHIVE_OK) { qCCritical(ARK) << "Error while writing" << filename << ":" << archive_error_string(dest) << "(error no =" << archive_errno(dest) << ')'; return; } if (partialprogress) { m_currentExtractedFilesSize += readBytes; emit progress(float(m_currentExtractedFilesSize) / m_extractedFilesSize); } readBytes = file.read(buff, sizeof(buff)); } file.close(); } void LibarchivePlugin::copyData(const QString& filename, struct archive *source, struct archive *dest, bool partialprogress) { char buff[10240]; ssize_t readBytes; readBytes = archive_read_data(source, buff, sizeof(buff)); while (readBytes > 0) { archive_write_data(dest, buff, readBytes); if (archive_errno(dest) != ARCHIVE_OK) { qCCritical(ARK) << "Error while extracting" << filename << ":" << archive_error_string(dest) << "(error no =" << archive_errno(dest) << ')'; return; } if (partialprogress) { m_currentExtractedFilesSize += readBytes; emit progress(float(m_currentExtractedFilesSize) / m_extractedFilesSize); } readBytes = archive_read_data(source, buff, sizeof(buff)); } } QString LibarchivePlugin::convertCompressionName(const QString &method) { if (method == QLatin1String("gzip")) { return QStringLiteral("GZip"); } else if (method == QLatin1String("bzip2")) { return QStringLiteral("BZip2"); } else if (method == QLatin1String("xz")) { return QStringLiteral("XZ"); } else if (method == QLatin1String("compress (.Z)")) { return QStringLiteral("Compress"); } else if (method == QLatin1String("lrzip")) { return QStringLiteral("LRZip"); } else if (method == QLatin1String("lzip")) { return QStringLiteral("LZip"); } else if (method == QLatin1String("lz4")) { return QStringLiteral("LZ4"); } else if (method == QLatin1String("lzop")) { return QStringLiteral("lzop"); } else if (method == QLatin1String("lzma")) { return QStringLiteral("LZMA"); } return QString(); } #include "libarchiveplugin.moc" diff --git a/plugins/libarchive/libarchiveplugin.h b/plugins/libarchive/libarchiveplugin.h index 85a5ec4b..2b00385f 100644 --- a/plugins/libarchive/libarchiveplugin.h +++ b/plugins/libarchive/libarchiveplugin.h @@ -1,102 +1,103 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef LIBARCHIVEPLUGIN_H #define LIBARCHIVEPLUGIN_H #include "kerfuffle/archiveinterface.h" #include "kerfuffle/archiveentry.h" #include #include using namespace Kerfuffle; class LibarchivePlugin : public ReadWriteArchiveInterface { Q_OBJECT public: explicit LibarchivePlugin(QObject *parent, const QVariantList &args); virtual ~LibarchivePlugin(); virtual bool list() Q_DECL_OVERRIDE; virtual bool doKill() Q_DECL_OVERRIDE; virtual bool extractFiles(const QVector &files, const QString &destinationDirectory, const ExtractionOptions &options) Q_DECL_OVERRIDE; virtual bool addFiles(const QVector &files, const Archive::Entry *destination, const CompressionOptions &options, uint numberOfEntriesToAdd = 0) Q_DECL_OVERRIDE; virtual bool moveFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) Q_DECL_OVERRIDE; virtual bool copyFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) Q_DECL_OVERRIDE; virtual bool deleteFiles(const QVector &files) Q_DECL_OVERRIDE; virtual bool addComment(const QString &comment) Q_DECL_OVERRIDE; virtual bool testArchive() Q_DECL_OVERRIDE; + virtual bool hasBatchExtractionProgress() const Q_DECL_OVERRIDE; protected: struct ArchiveReadCustomDeleter { static inline void cleanup(struct archive *a) { if (a) { archive_read_free(a); } } }; struct ArchiveWriteCustomDeleter { static inline void cleanup(struct archive *a) { if (a) { archive_write_free(a); } } }; typedef QScopedPointer ArchiveRead; typedef QScopedPointer ArchiveWrite; bool initializeReader(); void emitEntryFromArchiveEntry(struct archive_entry *entry); void copyData(const QString& filename, struct archive *dest, bool partialprogress = true); void copyData(const QString& filename, struct archive *source, struct archive *dest, bool partialprogress = true); ArchiveRead m_archiveReader; ArchiveRead m_archiveReadDisk; private: int extractionFlags() const; QString convertCompressionName(const QString &method); int m_cachedArchiveEntryCount; qlonglong m_currentExtractedFilesSize; bool m_emitNoEntries; qlonglong m_extractedFilesSize; QVector m_emittedEntries; }; #endif // LIBARCHIVEPLUGIN_H