diff --git a/app/main.cpp b/app/main.cpp index 79276205..511ba093 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -1,347 +1,347 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 Harald Hvaal * Copyright (C) 2015-2017 Ragnar Thomsen * * 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 "ark_version.h" #include "ark_debug.h" #include "mainwindow.h" #include "batchextract.h" #include "addtoarchive.h" #include "pluginmanager.h" #include #include #include #include #include #include #include #include #include using Kerfuffle::AddToArchive; class OpenFileEventHandler : public QObject { Q_OBJECT public: OpenFileEventHandler(QApplication *parent, MainWindow *w) : QObject(parent) , m_window(w) { parent->installEventFilter(this); } bool eventFilter(QObject *obj, QEvent *event) override { if (event->type() == QEvent::FileOpen) { QFileOpenEvent *openEvent = static_cast(event); qCDebug(ARK) << "File open event:" << openEvent->url() << "for window" << m_window; m_window->openUrl(openEvent->url()); return true; } return QObject::eventFilter(obj, event); } private: MainWindow *m_window; }; int main(int argc, char **argv) { QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); // Required for the webengine part. QApplication application(argc, argv); /** * enable high dpi support */ application.setAttribute(Qt::AA_UseHighDpiPixmaps, true); KCrash::initialize(); // Debug output can be turned on here: //QLoggingCategory::setFilterRules(QStringLiteral("ark.debug = true")); Kdelibs4ConfigMigrator migrate(QStringLiteral("ark")); migrate.setConfigFiles(QStringList() << QStringLiteral("arkrc")); migrate.setUiFiles(QStringList() << QStringLiteral("arkuirc")); migrate.migrate(); KLocalizedString::setApplicationDomain("ark"); KAboutData aboutData(QStringLiteral("ark"), i18n("Ark"), QStringLiteral(ARK_VERSION_STRING), i18n("KDE Archiving tool"), KAboutLicense::GPL, i18n("(c) 1997-2019, The Ark Developers"), QString(), QStringLiteral("https://utils.kde.org/projects/ark") ); aboutData.setOrganizationDomain("kde.org"); aboutData.addAuthor(i18n("Elvis Angelaccio"), i18n("Maintainer"), QStringLiteral("elvis.angelaccio@kde.org")); aboutData.addAuthor(i18n("Ragnar Thomsen"), i18n("Maintainer, KF5 port"), QStringLiteral("rthomsen6@gmail.com")); aboutData.addAuthor(i18n("Raphael Kubo da Costa"), i18n("Former Maintainer"), QStringLiteral("rakuco@FreeBSD.org")); aboutData.addAuthor(i18n("Harald Hvaal"), i18n("Former Maintainer"), QStringLiteral("haraldhv@stud.ntnu.no")); aboutData.addAuthor(i18n("Henrique Pinto"), i18n("Former Maintainer"), QStringLiteral("henrique.pinto@kdemail.net")); aboutData.addAuthor(i18n("Helio Chissini de Castro"), i18n("Former maintainer"), QStringLiteral("helio@kde.org")); aboutData.addAuthor(i18n("Georg Robbers"), QString(), QStringLiteral("Georg.Robbers@urz.uni-hd.de")); aboutData.addAuthor(i18n("Roberto Selbach Teixeira"), QString(), QStringLiteral("maragato@kde.org")); aboutData.addAuthor(i18n("Francois-Xavier Duranceau"), QString(), QStringLiteral("duranceau@kde.org")); aboutData.addAuthor(i18n("Emily Ezust (Corel Corporation)"), QString(), QStringLiteral("emilye@corel.com")); aboutData.addAuthor(i18n("Michael Jarrett (Corel Corporation)"), QString(), QStringLiteral("michaelj@corel.com")); aboutData.addAuthor(i18n("Robert Palmbos"), QString(), QStringLiteral("palm9744@kettering.edu")); aboutData.addCredit(i18n("Vladyslav Batyrenko"), i18n("Advanced editing functionalities"), QString(), QStringLiteral("https://mvlabat.github.io/ark-gsoc-2016/")); aboutData.addCredit(i18n("Bryce Corkins"), i18n("Icons"), QStringLiteral("dbryce@attglobal.net")); aboutData.addCredit(i18n("Liam Smit"), i18n("Ideas, help with the icons"), QStringLiteral("smitty@absamail.co.za")); aboutData.addCredit(i18n("Andrew Smith"), i18n("bkisofs code"), QString(), QStringLiteral("http://littlesvr.ca/misc/contactandrew.php")); KAboutData::setApplicationData(aboutData); application.setWindowIcon(QIcon::fromTheme(QStringLiteral("ark"), application.windowIcon())); QCommandLineParser parser; // Url to open. parser.addPositionalArgument(QStringLiteral("[urls]"), i18n("URLs to open.")); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("d") << QStringLiteral("dialog"), i18n("Show a dialog for specifying the options for the operation (extract/add)"))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("o") << QStringLiteral("destination"), i18n("Destination folder to extract to. Defaults to current path if not specified."), QStringLiteral("directory"))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("O") << QStringLiteral("opendestination"), i18n("Open destination folder after extraction."))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("c") << QStringLiteral("add"), i18n("Query the user for an archive filename and add specified files to it. Quit when finished."))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("t") << QStringLiteral("add-to"), i18n("Add the specified files to 'filename'. Create archive if it does not exist. Quit when finished."), QStringLiteral("filename"))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("p") << QStringLiteral("changetofirstpath"), i18n("Change the current dir to the first entry and add all other entries relative to this one."))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("f") << QStringLiteral("autofilename"), i18n("Automatically choose a filename, with the selected suffix (for example rar, tar.gz, zip or any other supported types)"), QStringLiteral("suffix"))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("b") << QStringLiteral("batch"), i18n("Use the batch interface instead of the usual dialog. This option is implied if more than one url is specified."))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("e") << QStringLiteral("autodestination"), i18n("The destination argument will be set to the path of the first file supplied."))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("a") << QStringLiteral("autosubfolder"), - i18n("Archive contents will be read, and if detected to not be a single folder archive, a subfolder with the name of the archive will be created."))); + i18n("Archive contents will be read, and if detected to not be a single folder or a single file archive, a subfolder with the name of the archive will be created."))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("m") << QStringLiteral("mimetypes"), i18n("List supported MIME types."))); aboutData.setupCommandLine(&parser); // Do the command line parsing. parser.process(application); // Handle standard options. aboutData.processCommandLine(&parser); // This is needed to prevent Dolphin from freezing when opening an archive. KDBusService dbusService(KDBusService::Multiple | KDBusService::NoExitOnFailure); // Session restoring. if (application.isSessionRestored()) { if (!KMainWindow::canBeRestored(1)) { return -1; } MainWindow* window = new MainWindow; window->restore(1); if (!window->loadPart()) { delete window; return -1; } } else { // New ark window (no restored session). // Open any given URLs. const QStringList urls = parser.positionalArguments(); if (parser.isSet(QStringLiteral("add")) || parser.isSet(QStringLiteral("add-to"))) { AddToArchive *addToArchiveJob = new AddToArchive(&application); application.setQuitOnLastWindowClosed(false); QObject::connect(addToArchiveJob, &KJob::result, &application, &QCoreApplication::quit, Qt::QueuedConnection); if (parser.isSet(QStringLiteral("changetofirstpath"))) { qCDebug(ARK) << "Setting changetofirstpath"; addToArchiveJob->setChangeToFirstPath(true); } if (parser.isSet(QStringLiteral("add-to"))) { qCDebug(ARK) << "Setting filename to" << parser.value(QStringLiteral("add-to")); addToArchiveJob->setFilename(QUrl::fromUserInput(parser.value(QStringLiteral("add-to")), QString(), QUrl::AssumeLocalFile)); } if (parser.isSet(QStringLiteral("autofilename"))) { qCDebug(ARK) << "Setting autofilename to" << parser.value(QStringLiteral("autofilename")); addToArchiveJob->setAutoFilenameSuffix(parser.value(QStringLiteral("autofilename"))); } for (int i = 0; i < urls.count(); ++i) { //TODO: use the returned value here? qCDebug(ARK) << "Adding url" << QUrl::fromUserInput(urls.at(i), QString(), QUrl::AssumeLocalFile); addToArchiveJob->addInput(QUrl::fromUserInput(urls.at(i), QString(), QUrl::AssumeLocalFile)); } if (parser.isSet(QStringLiteral("dialog"))) { qCDebug(ARK) << "Using kerfuffle to open add dialog"; if (!addToArchiveJob->showAddDialog()) { return 0; } } addToArchiveJob->start(); } else if (parser.isSet(QStringLiteral("batch"))) { if (urls.isEmpty()) { qCDebug(ARK) << "No urls to be extracted were provided."; parser.showHelp(-1); } BatchExtract *batchJob = new BatchExtract(&application); application.setQuitOnLastWindowClosed(false); QObject::connect(batchJob, &KJob::result, &application, &QCoreApplication::quit, Qt::QueuedConnection); for (int i = 0; i < urls.count(); ++i) { qCDebug(ARK) << "Adding url" << QUrl::fromUserInput(urls.at(i), QString(), QUrl::AssumeLocalFile); batchJob->addInput(QUrl::fromUserInput(urls.at(i), QString(), QUrl::AssumeLocalFile)); } if (parser.isSet(QStringLiteral("autosubfolder"))) { qCDebug(ARK) << "Setting autosubfolder"; batchJob->setAutoSubfolder(true); } if (parser.isSet(QStringLiteral("autodestination"))) { QString autopath = QFileInfo(QUrl::fromUserInput(urls.at(0), QString(), QUrl::AssumeLocalFile).path()).path(); qCDebug(ARK) << "By autodestination, setting path to " << autopath; batchJob->setDestinationFolder(autopath); } if (parser.isSet(QStringLiteral("destination"))) { qCDebug(ARK) << "Setting destination to " << parser.value(QStringLiteral("destination")); batchJob->setDestinationFolder(parser.value(QStringLiteral("destination"))); } if (parser.isSet(QStringLiteral("opendestination"))) { qCDebug(ARK) << "Setting opendestination"; batchJob->setOpenDestinationAfterExtraction(true); } if (parser.isSet(QStringLiteral("dialog"))) { qCDebug(ARK) << "Opening extraction dialog"; if (!batchJob->showExtractDialog()) { return 0; } } batchJob->start(); } else if (parser.isSet(QStringLiteral("mimetypes"))) { Kerfuffle::PluginManager pluginManager; const auto mimeTypes = pluginManager.supportedMimeTypes(); QTextStream cout(stdout); for (const auto &mimeType : mimeTypes) { cout << mimeType << '\n'; } return 0; } else { MainWindow *window = new MainWindow; if (!window->loadPart()) { // if loading the part fails delete window; return -1; } if (!urls.isEmpty()) { qCDebug(ARK) << "Trying to open" << QUrl::fromUserInput(urls.at(0), QString(), QUrl::AssumeLocalFile); if (parser.isSet(QStringLiteral("dialog"))) { window->setShowExtractDialog(true); } window->openUrl(QUrl::fromUserInput(urls.at(0), QString(), QUrl::AssumeLocalFile)); } new OpenFileEventHandler(&application, window); window->show(); } } qCDebug(ARK) << "Entering application loop"; return application.exec(); } #include "main.moc" diff --git a/autotests/app/batchextracttest.cpp b/autotests/app/batchextracttest.cpp index 7e9a864f..80930d3c 100644 --- a/autotests/app/batchextracttest.cpp +++ b/autotests/app/batchextracttest.cpp @@ -1,131 +1,131 @@ /* * Copyright (c) 2016 Elvis Angelaccio * * 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 "batchextract.h" #include #include class BatchExtractTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testBatchExtraction_data(); void testBatchExtraction(); private: QString m_expectedWorkingDir; }; QTEST_MAIN(BatchExtractTest) void BatchExtractTest::initTestCase() { // #395939: after each extraction, the cwd must be the one we started from. m_expectedWorkingDir = QDir::currentPath(); } void BatchExtractTest::testBatchExtraction_data() { QTest::addColumn("archivePath"); QTest::addColumn("autoSubfolder"); // Expected numbers of entries (files + folders) in the temporary extraction folder. // This is the number of entries in the archive (+ 1, if the autosubfolder is expected). QTest::addColumn("expectedExtractedEntriesCount"); QTest::newRow("extract the whole simple%archive.tar.gz (bug #365798)") << QFINDTESTDATA("data/simple%archive.tar.gz") << true << 5; QTest::newRow("single-folder, no autosubfolder") << QFINDTESTDATA("../kerfuffle/data/one_toplevel_folder.zip") << false << 9; QTest::newRow("single-folder, autosubfolder") << QFINDTESTDATA("../kerfuffle/data/one_toplevel_folder.zip") << true << 9; QTest::newRow("non single-folder, no autosubfolder") << QFINDTESTDATA("../kerfuffle/data/simplearchive.tar.gz") << false << 4; QTest::newRow("non single-folder, autosubfolder") << QFINDTESTDATA("../kerfuffle/data/simplearchive.tar.gz") << true << 5; QTest::newRow("single-file, no autosubfolder") << QFINDTESTDATA("data/test.txt.gz") << false << 1; QTest::newRow("single-file, autosubfolder") << QFINDTESTDATA("data/test.txt.gz") << true - << 2; + << 1; } void BatchExtractTest::testBatchExtraction() { auto batchJob = new BatchExtract(this); QFETCH(QString, archivePath); batchJob->addInput(QUrl::fromUserInput(archivePath)); QFETCH(bool, autoSubfolder); batchJob->setAutoSubfolder(autoSubfolder); QTemporaryDir destDir; if (!destDir.isValid()) { QSKIP("Could not create a temporary directory for extraction. Skipping test.", SkipSingle); } batchJob->setDestinationFolder(destDir.path()); QEventLoop eventLoop(this); connect(batchJob, &KJob::result, &eventLoop, &QEventLoop::quit); batchJob->start(); eventLoop.exec(); // krazy:exclude=crashy QFETCH(int, expectedExtractedEntriesCount); int extractedEntriesCount = 0; QDirIterator dirIt(destDir.path(), QDir::AllEntries | QDir::Hidden | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (dirIt.hasNext()) { extractedEntriesCount++; dirIt.next(); } QCOMPARE(extractedEntriesCount, expectedExtractedEntriesCount); QCOMPARE(QDir::currentPath(), m_expectedWorkingDir); } #include "batchextracttest.moc" diff --git a/kerfuffle/jobs.cpp b/kerfuffle/jobs.cpp index 9f7da604..0ec7be24 100644 --- a/kerfuffle/jobs.cpp +++ b/kerfuffle/jobs.cpp @@ -1,852 +1,852 @@ /* * 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 = nullptr) : QThread(parent) , q(job) { } void run() 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, nullptr) {} Job::Job(ReadOnlyArchiveInterface *interface) : Job(nullptr, interface) {} Job::~Job() { 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); } if (!d->isInterruptionRequested()) { 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() { const bool killed = archiveInterface()->doKill(); if (killed) { return true; } if (d->isRunning()) { qCDebug(ARK) << "Requesting graceful thread interruption, will abort in one second otherwise."; d->requestInterruption(); d->wait(1000); } return true; } 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) << "Created job instance"; connect(this, &LoadJob::newEntry, this, &LoadJob::onNewEntry); } LoadJob::LoadJob(Archive *archive) : LoadJob(archive, nullptr) {} LoadJob::LoadJob(ReadOnlyArchiveInterface *interface) : LoadJob(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() && result) { 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().remove(QRegularExpression(QStringLiteral("^\\./"))); 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) << "Created job instance"; } void BatchExtractJob::doWork() { connect(m_loadJob, &KJob::result, this, &BatchExtractJob::slotLoadingFinished); connect(archiveInterface(), &ReadOnlyArchiveInterface::cancelled, this, &BatchExtractJob::onCancelled); 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(); } bool BatchExtractJob::doKill() { if (m_step == Loading) { return m_loadJob->kill(); } return m_extractJob->kill(); } 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); m_extractJob = archive()->extractFiles({}, m_destination, options); if (m_extractJob) { connect(m_extractJob, &KJob::result, this, &BatchExtractJob::emitResult); connect(m_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); } m_step = Extracting; m_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)) { + if (m_autoSubfolder && (archive()->hasMultipleTopLevelEntries() || 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 == QLatin1String("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) << "Created job instance"; } void CreateJob::enableEncryption(const QString &password, bool encryptHeader) { archive()->encrypt(password, encryptHeader); } void CreateJob::setMultiVolume(bool isMultiVolume) { archive()->setMultiVolume(isMultiVolume); } void CreateJob::doWork() { connect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &CreateJob::onProgress); m_addJob = archive()->addFiles(m_entries, nullptr, m_options); if (m_addJob) { connect(m_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(m_addJob, &KJob::description, this, [=](KJob *, const QString &title, const QPair &field1, const QPair &) { emit description(this, title, field1); }); m_addJob->start(); } else { emitResult(); } } bool CreateJob::doKill() { return m_addJob && m_addJob->kill(); } 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) << "Created job instance"; } 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()), qMakePair(i18nc("extraction folder", "Destination"), m_destinationDir)); 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() { // pass 1 to i18np on purpose so this translation may properly be reused. emit description(this, i18np("Extracting one file", "Extracting %1 files", 1)); 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) << "Created job instance"; } OpenJob::OpenJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : TempExtractJob(entry, passwordProtectedHint, interface) { qCDebug(ARK) << "Created job instance"; } OpenWithJob::OpenWithJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : OpenJob(entry, passwordProtectedHint, interface) { qCDebug(ARK) << "Created job instance"; } 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) << "Created job instance"; } 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. uint totalCount = 0; QElapsedTimer timer; timer.start(); for (const Archive::Entry* entry : qAsConst(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) << "Going to add" << totalCount << "entries, counted in" << timer.elapsed() << "ms"; const QString desc = i18np("Compressing a file", "Compressing %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. for (Archive::Entry *entry : qAsConst(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) << "Created job instance"; } void MoveJob::doWork() { qCDebug(ARK) << "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) << "Created job instance"; } void CopyJob::doWork() { qCDebug(ARK) << "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) << "Job 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"