diff --git a/app/batchextract.h b/app/batchextract.h --- a/app/batchextract.h +++ b/app/batchextract.h @@ -29,9 +29,10 @@ #ifndef BATCHEXTRACT_H #define BATCHEXTRACT_H -#include +#include #include +#include namespace Kerfuffle { @@ -63,15 +64,15 @@ virtual ~BatchExtract(); /** - * Creates an ExtractJob for the given @p archive and puts it on the queue. + * Creates a BatchExtractJob for the given @p url and puts it on the queue. * - * If necessary, the destination directory for the archive is created. + * If necessary, the destination directory for the archive is created by the job. * - * @param archive The archive that will be extracted. + * @param url The url of the archive that will be extracted. * * @see setAutoSubfolder */ - void addExtraction(Kerfuffle::Archive* archive); + void addExtraction(const QUrl& url); /** * A wrapper that calls slotStartJob() when the event loop has started. @@ -106,13 +107,8 @@ * Adds a file to the list of files that will be extracted. * * @param url The file that will be added to the list. - * - * @return @c true The file exists and a suitable plugin - * could be found for it. - * @return @c false The file does not exist or a suitable - * plugin could not be found. */ - bool addInput(const QUrl& url); + void addInput(const QUrl& url); /** * Shows the extract options dialog before extracting the files. @@ -220,7 +216,7 @@ QMap > m_fileNames; bool m_autoSubfolder; - QList m_inputs; + QVector m_inputs; QString m_destinationFolder; QStringList m_failedFiles; bool m_preservePaths; diff --git a/app/batchextract.cpp b/app/batchextract.cpp --- a/app/batchextract.cpp +++ b/app/batchextract.cpp @@ -28,18 +28,16 @@ #include "batchextract.h" #include "ark_debug.h" -#include "kerfuffle/archive_kerfuffle.h" #include "kerfuffle/extractiondialog.h" #include "kerfuffle/jobs.h" #include "kerfuffle/queries.h" +#include #include #include #include -#include #include -#include #include #include #include @@ -64,46 +62,21 @@ } } -void BatchExtract::addExtraction(Kerfuffle::Archive* archive) +void BatchExtract::addExtraction(const QUrl& url) { QString destination = destinationFolder(); - const bool isSingleFolderRPM = (archive->isSingleFolderArchive() && - (archive->mimeType().name() == QLatin1String("application/x-rpm"))); - - if ((autoSubfolder()) && (!archive->isSingleFolderArchive() || isSingleFolderRPM)) { - const QDir d(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(destination, QString(), QUrl::AssumeLocalFile), subfolderName); - } - d.mkdir(subfolderName); - - destination += QLatin1Char( '/' ) + subfolderName; - } + auto job = Kerfuffle::Archive::batchExtract(url.toLocalFile(), destination, autoSubfolder(), preservePaths()); - Kerfuffle::ExtractionOptions options; - options[QStringLiteral("PreservePaths")] = preservePaths(); - - Kerfuffle::ExtractJob *job = archive->extractFiles(QList(), destination, options); - - qCDebug(ARK) << QString(QStringLiteral("Registering job from archive %1, to %2, preservePaths %3")).arg(archive->fileName(), destination, QString::number(preservePaths())); + qCDebug(ARK) << QString(QStringLiteral("Registering job from archive %1, to %2, preservePaths %3")).arg(url.toLocalFile(), destination, QString::number(preservePaths())); addSubjob(job); - m_fileNames[job] = qMakePair(archive->fileName(), destination); + m_fileNames[job] = qMakePair(url.toLocalFile(), destination); connect(job, SIGNAL(percent(KJob*,ulong)), this, SLOT(forwardProgress(KJob*,ulong))); - connect(job, &Kerfuffle::Job::userQuery, + connect(job, &Kerfuffle::BatchExtractJob::userQuery, this, &BatchExtract::slotUserQuery); } @@ -129,14 +102,13 @@ void BatchExtract::slotStartJob() { - // If none of the archives could be loaded, there is no subjob to run if (m_inputs.isEmpty()) { emitResult(); return; } - foreach(Kerfuffle::Archive *archive, m_inputs) { - addExtraction(archive); + foreach (const auto& url, m_inputs) { + addExtraction(url); } KIO::getJobTracker()->registerJob(this); @@ -167,25 +139,24 @@ // TODO: The user must be informed about which file caused the error, and that the other files // in the queue will not be extracted. if (job->error()) { - qCDebug(ARK) << "There was en error:" << job->error() << ", errorText:" << job->errorText(); + qCDebug(ARK) << "There was en error:" << job->error() << ", errorText:" << job->errorString(); - setErrorText(job->errorText()); + setErrorText(job->errorString()); setError(job->error()); removeSubjob(job); if (job->error() != KJob::KilledJobError) { - KMessageBox::error(NULL, job->errorText().isEmpty() ? - i18n("There was an error during extraction.") : job->errorText()); + KMessageBox::error(Q_NULLPTR, job->errorString().isEmpty() ? + i18n("There was an error during extraction.") : job->errorString()); } emitResult(); - return; - } else { - removeSubjob(job); } + removeSubjob(job); + if (!hasSubjobs()) { if (openDestinationAfterExtraction()) { QUrl destination(destinationFolder()); @@ -213,21 +184,16 @@ setPercent(jobPart *(m_initialJobCount - subjobs().size()) + percent / m_initialJobCount); } -bool BatchExtract::addInput(const QUrl& url) +void BatchExtract::addInput(const QUrl& url) { qCDebug(ARK) << "Adding archive" << url.toLocalFile(); - Kerfuffle::Archive *archive = Kerfuffle::Archive::create(url.toLocalFile(), this); - Q_ASSERT(archive); - if (!QFileInfo::exists(url.toLocalFile())) { m_failedFiles.append(url.fileName()); - return false; + return; } - m_inputs.append(archive); - - return true; + m_inputs.append(url); } bool BatchExtract::openDestinationAfterExtraction() const @@ -280,14 +246,36 @@ dialog.data()->setCurrentUrl(QUrl::fromUserInput(destinationFolder(), QString(), QUrl::AssumeLocalFile)); dialog.data()->setPreservePaths(preservePaths()); + // Only one archive, we need a LoadJob to get the single-folder and subfolder properties. + // TODO: find a better way (e.g. let the dialog handle everything), otherwise we list + // the archive twice (once here and once in the following BatchExtractJob). + Kerfuffle::LoadJob *loadJob = Q_NULLPTR; if (m_inputs.size() == 1) { - if (m_inputs.at(0)->isSingleFolderArchive()) { - dialog.data()->setSingleFolderArchive(true); - } - dialog.data()->setSubfolder(m_inputs.at(0)->subfolderName()); + loadJob = Kerfuffle::Archive::load(m_inputs.at(0).toLocalFile(), this); + // We need to access the job after result has been emitted, if the user rejects the dialog. + loadJob->setAutoDelete(false); + + connect(loadJob, &KJob::result, this, [=](KJob *job) { + if (job->error()) { + return; + } + + auto archive = qobject_cast(job)->archive(); + dialog->setSingleFolderArchive(archive->isSingleFolder()); + dialog->setSubfolder(archive->subfolderName()); + }); + + connect(loadJob, &KJob::result, dialog.data(), &Kerfuffle::ExtractionDialog::setReadyGui); + dialog->setBusyGui(); + // NOTE: we exploit the dialog->exec() below to run this job. + loadJob->start(); } if (!dialog.data()->exec()) { + if (loadJob) { + loadJob->kill(); + loadJob->deleteLater(); + } delete dialog.data(); return false; } diff --git a/autotests/kerfuffle/addtest.cpp b/autotests/kerfuffle/addtest.cpp --- a/autotests/kerfuffle/addtest.cpp +++ b/autotests/kerfuffle/addtest.cpp @@ -23,7 +23,7 @@ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#include "autotests/testhelper/testhelper.h" +#include "testhelper.h" using namespace Kerfuffle; @@ -106,7 +106,12 @@ QFETCH(QString, archiveName); const QString archivePath = temporaryDir.path() + QLatin1Char('/') + archiveName; Q_ASSERT(QFile::copy(QFINDTESTDATA(QStringLiteral("data/") + archiveName), archivePath)); - Archive *archive = Archive::create(archivePath, this); + + auto loadJob = Archive::load(archivePath); + QVERIFY(loadJob); + + TestHelper::startAndWaitForResult(loadJob); + auto archive = loadJob->archive(); QVERIFY(archive); if (!archive->isValid()) { diff --git a/autotests/kerfuffle/addtoarchivetest.cpp b/autotests/kerfuffle/addtoarchivetest.cpp --- a/autotests/kerfuffle/addtoarchivetest.cpp +++ b/autotests/kerfuffle/addtoarchivetest.cpp @@ -23,11 +23,10 @@ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#include "kerfuffle/addtoarchive.h" -#include "kerfuffle/archive_kerfuffle.h" -#include "kerfuffle/pluginmanager.h" +#include "addtoarchive.h" +#include "pluginmanager.h" +#include "testhelper.h" -#include #include #include #include @@ -43,7 +42,6 @@ void init(); void testCompressHere_data(); void testCompressHere(); - void testCreateEncryptedArchive(); }; void AddToArchiveTest::init() @@ -55,41 +53,47 @@ void AddToArchiveTest::testCompressHere_data() { QTest::addColumn("expectedSuffix"); + QTest::addColumn("expectedEncryptionType"); QTest::addColumn("inputFiles"); QTest::addColumn("expectedArchiveName"); QTest::addColumn("expectedNumberOfFiles"); QTest::addColumn("expectedNumberOfFolders"); QTest::newRow("compress here (as TAR) - dir with files") << QStringLiteral("tar.gz") + << Archive::Unencrypted << QStringList {QFINDTESTDATA("data/testdir")} << QStringLiteral("testdir.tar.gz") << 2ULL << 1ULL; QTest::newRow("compress here (as TAR) - dir with subdirs") << QStringLiteral("tar.gz") + << Archive::Unencrypted << QStringList {QFINDTESTDATA("data/testdirwithsubdirs")} << QStringLiteral("testdirwithsubdirs.tar.gz") << 4ULL << 4ULL; QTest::newRow("compress here (as TAR) - dir with empty subdir") << QStringLiteral("tar.gz") + << Archive::Unencrypted << QStringList {QFINDTESTDATA("data/testdirwithemptysubdir")} << QStringLiteral("testdirwithemptysubdir.tar.gz") << 2ULL << 2ULL; QTest::newRow("compress here (as TAR) - single file") << QStringLiteral("tar.gz") + << Archive::Unencrypted << QStringList {QFINDTESTDATA("data/testfile.txt")} << QStringLiteral("testfile.tar.gz") << 1ULL << 0ULL; QTest::newRow("compress here (as TAR) - file + folder") << QStringLiteral("tar.gz") + << Archive::Unencrypted << QStringList { QFINDTESTDATA("data/testdir"), QFINDTESTDATA("data/testfile.txt") @@ -100,42 +104,48 @@ QTest::newRow("compress here (as TAR) - bug #362690") << QStringLiteral("tar.gz") + << Archive::Unencrypted << QStringList {QFINDTESTDATA("data/test-3.4.0")} << QStringLiteral("test-3.4.0.tar.gz") << 1ULL << 1ULL; if (!PluginManager().preferredWritePluginsFor(QMimeDatabase().mimeTypeForName(QStringLiteral("application/zip"))).isEmpty()) { QTest::newRow("compress here (as ZIP) - dir with files") << QStringLiteral("zip") + << Archive::Unencrypted << QStringList {QFINDTESTDATA("data/testdir")} << QStringLiteral("testdir.zip") << 2ULL << 1ULL; QTest::newRow("compress here (as ZIP) - dir with subdirs") << QStringLiteral("zip") + << Archive::Unencrypted << QStringList {QFINDTESTDATA("data/testdirwithsubdirs")} << QStringLiteral("testdirwithsubdirs.zip") << 4ULL << 4ULL; QTest::newRow("compress here (as ZIP) - dir with empty subdir") << QStringLiteral("zip") + << Archive::Unencrypted << QStringList {QFINDTESTDATA("data/testdirwithemptysubdir")} << QStringLiteral("testdirwithemptysubdir.zip") << 2ULL << 2ULL; QTest::newRow("compress here (as ZIP) - single file") << QStringLiteral("zip") + << Archive::Unencrypted << QStringList {QFINDTESTDATA("data/testfile.txt")} << QStringLiteral("testfile.zip") << 1ULL << 0ULL; QTest::newRow("compress here (as ZIP) - file + folder") << QStringLiteral("zip") + << Archive::Unencrypted << QStringList { QFINDTESTDATA("data/testdir"), QFINDTESTDATA("data/testfile.txt") @@ -146,6 +156,7 @@ QTest::newRow("compress here (as TAR) - dir with special name (see #365798)") << QStringLiteral("tar.gz") + << Archive::Unencrypted << QStringList {QFINDTESTDATA("data/test%dir")} << QStringLiteral("test%dir.tar.gz") << 2ULL @@ -157,34 +168,50 @@ if (!PluginManager().preferredWritePluginsFor(QMimeDatabase().mimeTypeForName(QStringLiteral("application/vnd.rar"))).isEmpty()) { QTest::newRow("compress here (as RAR) - dir with files") << QStringLiteral("rar") + << Archive::Unencrypted << QStringList {QFINDTESTDATA("data/testdir")} << QStringLiteral("testdir.rar") << 2ULL << 1ULL; QTest::newRow("compress here (as RAR) - dir with subdirs") << QStringLiteral("rar") + << Archive::Unencrypted << QStringList {QFINDTESTDATA("data/testdirwithsubdirs")} << QStringLiteral("testdirwithsubdirs.rar") << 4ULL << 4ULL; QTest::newRow("compress here (as RAR) - dir with empty subdir") << QStringLiteral("rar") + << Archive::Unencrypted << QStringList {QFINDTESTDATA("data/testdirwithemptysubdir")} << QStringLiteral("testdirwithemptysubdir.rar") << 2ULL << 2ULL; QTest::newRow("compress here (as RAR) - single file") << QStringLiteral("rar") + << Archive::Unencrypted << QStringList {QFINDTESTDATA("data/testfile.txt")} << QStringLiteral("testfile.rar") << 1ULL << 0ULL; QTest::newRow("compress here (as RAR) - file + folder") << QStringLiteral("rar") + << Archive::Unencrypted + << QStringList { + QFINDTESTDATA("data/testdir"), + QFINDTESTDATA("data/testfile.txt") + } + << QStringLiteral("data.rar") + << 3ULL + << 1ULL; + + QTest::newRow("compress to encrypted RAR - file + folder") + << QStringLiteral("rar") + << Archive::Encrypted << QStringList { QFINDTESTDATA("data/testdir"), QFINDTESTDATA("data/testfile.txt") @@ -205,47 +232,40 @@ QFETCH(QString, expectedSuffix); addToArchiveJob->setAutoFilenameSuffix(expectedSuffix); + QFETCH(Archive::EncryptionType, expectedEncryptionType); + if (expectedEncryptionType == Archive::Encrypted) { + addToArchiveJob->setPassword(QLatin1String("1234")); + } + QFETCH(QStringList, inputFiles); foreach (const QString &file, inputFiles) { addToArchiveJob->addInput(QUrl::fromUserInput(file)); } - // Run the job in the following event loop. - QEventLoop eventLoop(this); - connect(addToArchiveJob, &KJob::result, &eventLoop, &QEventLoop::quit); - addToArchiveJob->start(); - eventLoop.exec(); // krazy:exclude=crashy + // Run the job. + TestHelper::startAndWaitForResult(addToArchiveJob); // Check the properties of the generated test archive, then remove it. QFETCH(QString, expectedArchiveName); - Archive *archive = Archive::create(QFINDTESTDATA(QStringLiteral("data/%1").arg(expectedArchiveName))); + auto loadJob = Archive::load(QFINDTESTDATA(QStringLiteral("data/%1").arg(expectedArchiveName))); + QVERIFY(loadJob); + + TestHelper::startAndWaitForResult(loadJob); + auto archive = loadJob->archive(); QVERIFY(archive); QVERIFY(archive->isValid()); QCOMPARE(archive->completeBaseName() + QLatin1Char('.') + expectedSuffix, expectedArchiveName); + QCOMPARE(archive->encryptionType(), expectedEncryptionType); + QFETCH(qulonglong, expectedNumberOfFiles); QCOMPARE(archive->numberOfFiles(), expectedNumberOfFiles); QFETCH(qulonglong, expectedNumberOfFolders); QCOMPARE(archive->numberOfFolders(), expectedNumberOfFolders); QVERIFY(QFile(archive->fileName()).remove()); -} - -void AddToArchiveTest::testCreateEncryptedArchive() -{ - Archive *archive = Archive::create(QStringLiteral("foo.zip")); - QVERIFY(archive); - - if (!archive->isValid()) { - QSKIP("Could not find a plugin to handle the archive. Skipping test.", SkipSingle); - } - - QCOMPARE(archive->encryptionType(), Archive::Unencrypted); - - archive->encrypt(QStringLiteral("1234"), false); - QCOMPARE(archive->encryptionType(), Archive::Encrypted); archive->deleteLater(); } diff --git a/autotests/kerfuffle/copytest.cpp b/autotests/kerfuffle/copytest.cpp --- a/autotests/kerfuffle/copytest.cpp +++ b/autotests/kerfuffle/copytest.cpp @@ -23,7 +23,7 @@ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#include "autotests/testhelper/testhelper.h" +#include "testhelper.h" using namespace Kerfuffle; @@ -173,7 +173,12 @@ QFETCH(QString, archiveName); const QString archivePath = temporaryDir.path() + QLatin1Char('/') + archiveName; Q_ASSERT(QFile::copy(QFINDTESTDATA(QStringLiteral("data/") + archiveName), archivePath)); - Archive *archive = Archive::create(archivePath, this); + + auto loadJob = Archive::load(archivePath); + QVERIFY(loadJob); + + TestHelper::startAndWaitForResult(loadJob); + auto archive = loadJob->archive(); QVERIFY(archive); if (!archive->isValid()) { diff --git a/autotests/kerfuffle/extracttest.cpp b/autotests/kerfuffle/extracttest.cpp --- a/autotests/kerfuffle/extracttest.cpp +++ b/autotests/kerfuffle/extracttest.cpp @@ -24,8 +24,7 @@ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#include "kerfuffle/archive_kerfuffle.h" -#include "kerfuffle/jobs.h" +#include "testhelper.h" #include #include @@ -62,8 +61,8 @@ QTest::newRow("non-existent tar archive") << QStringLiteral("/tmp/foo.tar.gz") << QStringLiteral("foo") - << false << false << false << false << 0 << Archive::Unencrypted - << QString(); + << false << false << true << false << 0 << Archive::Unencrypted + << QStringLiteral("foo"); // Test non-archive file QTest::newRow("not an archive") @@ -195,7 +194,11 @@ void ExtractTest::testProperties() { QFETCH(QString, archivePath); - Archive *archive = Archive::create(archivePath, this); + auto loadJob = Archive::load(archivePath, this); + QVERIFY(loadJob); + + TestHelper::startAndWaitForResult(loadJob); + auto archive = loadJob->archive(); QVERIFY(archive); if (!archive->isValid()) { @@ -218,7 +221,7 @@ } QFETCH(bool, isSingleFolder); - QCOMPARE(archive->isSingleFolderArchive(), isSingleFolder); + QCOMPARE(archive->isSingleFolder(), isSingleFolder); QFETCH(bool, isMultiVolume); QCOMPARE(archive->isMultiVolume(), isMultiVolume); @@ -589,7 +592,12 @@ void ExtractTest::testExtraction() { QFETCH(QString, archivePath); - Archive *archive = Archive::create(archivePath, this); + auto loadJob = Archive::load(archivePath, this); + QVERIFY(loadJob); + + Archive *archive = Q_NULLPTR; + TestHelper::startAndWaitForResult(loadJob); + archive = loadJob->archive(); QVERIFY(archive); if (!archive->isValid()) { @@ -605,10 +613,7 @@ QFETCH(ExtractionOptions, extractionOptions); auto extractionJob = archive->extractFiles(entriesToExtract, destDir.path(), extractionOptions); - QEventLoop eventLoop(this); - connect(extractionJob, &KJob::result, &eventLoop, &QEventLoop::quit); - extractionJob->start(); - eventLoop.exec(); // krazy:exclude=crashy + TestHelper::startAndWaitForResult(extractionJob); QFETCH(int, expectedExtractedEntriesCount); int extractedEntriesCount = 0; diff --git a/autotests/kerfuffle/jobstest.cpp b/autotests/kerfuffle/jobstest.cpp --- a/autotests/kerfuffle/jobstest.cpp +++ b/autotests/kerfuffle/jobstest.cpp @@ -47,8 +47,8 @@ private Q_SLOTS: // ListJob-related tests - void testListJob_data(); - void testListJob(); + void testLoadJob_data(); + void testLoadJob(); // ExtractJob-related tests void testExtractJobAccessors(); @@ -104,11 +104,11 @@ { m_entries.clear(); - ListJob *listJob = new ListJob(iface); - connect(listJob, &Job::newEntry, + auto job = new LoadJob(iface); + connect(job, &Job::newEntry, this, &JobsTest::slotNewEntry); - startAndWaitForResult(listJob); + startAndWaitForResult(job); return m_entries; } @@ -120,7 +120,7 @@ m_eventLoop.exec(); } -void JobsTest::testListJob_data() +void JobsTest::testLoadJob_data() { QTest::addColumn("jsonArchive"); QTest::addColumn("expectedExtractedFilesSize"); @@ -184,24 +184,24 @@ }; } -void JobsTest::testListJob() +void JobsTest::testLoadJob() { QFETCH(QString, jsonArchive); JSONArchiveInterface *iface = createArchiveInterface(jsonArchive); QVERIFY(iface); - ListJob *listJob = new ListJob(iface); - listJob->setAutoDelete(false); - startAndWaitForResult(listJob); + auto loadJob = new LoadJob(iface); + loadJob->setAutoDelete(false); + startAndWaitForResult(loadJob); QFETCH(qlonglong, expectedExtractedFilesSize); - QCOMPARE(listJob->extractedFilesSize(), expectedExtractedFilesSize); + QCOMPARE(loadJob->extractedFilesSize(), expectedExtractedFilesSize); QFETCH(bool, isPasswordProtected); - QCOMPARE(listJob->isPasswordProtected(), isPasswordProtected); + QCOMPARE(loadJob->isPasswordProtected(), isPasswordProtected); QFETCH(bool, isSingleFolder); - QCOMPARE(listJob->isSingleFolderArchive(), isSingleFolder); + QCOMPARE(loadJob->isSingleFolderArchive(), isSingleFolder); QFETCH(QStringList, expectedEntryNames); auto archiveEntries = listEntries(iface); @@ -212,7 +212,7 @@ QCOMPARE(archiveEntries.at(i)->fullPath(), expectedEntryNames.at(i)); } - listJob->deleteLater(); + loadJob->deleteLater(); } void JobsTest::testExtractJobAccessors() diff --git a/autotests/kerfuffle/movetest.cpp b/autotests/kerfuffle/movetest.cpp --- a/autotests/kerfuffle/movetest.cpp +++ b/autotests/kerfuffle/movetest.cpp @@ -23,7 +23,7 @@ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#include "autotests/testhelper/testhelper.h" +#include "testhelper.h" using namespace Kerfuffle; @@ -151,7 +151,12 @@ QFETCH(QString, archiveName); const QString archivePath = temporaryDir.path() + QLatin1Char('/') + archiveName; Q_ASSERT(QFile::copy(QFINDTESTDATA(QStringLiteral("data/") + archiveName), archivePath)); - Archive *archive = Archive::create(archivePath, this); + + auto loadJob = Archive::load(archivePath); + QVERIFY(loadJob); + + TestHelper::startAndWaitForResult(loadJob); + auto archive = loadJob->archive(); QVERIFY(archive); if (!archive->isValid()) { diff --git a/autotests/plugins/cli7zplugin/cli7ztest.cpp b/autotests/plugins/cli7zplugin/cli7ztest.cpp --- a/autotests/plugins/cli7zplugin/cli7ztest.cpp +++ b/autotests/plugins/cli7zplugin/cli7ztest.cpp @@ -24,14 +24,14 @@ */ #include "cli7ztest.h" +#include "testhelper.h" #include #include #include #include #include -#include QTEST_GUILESS_MAIN(Cli7zTest) @@ -72,7 +72,11 @@ } QFETCH(QString, archivePath); - Archive *archive = Archive::create(archivePath, m_plugin, this); + auto loadJob = Archive::load(archivePath, m_plugin, this); + QVERIFY(loadJob); + + TestHelper::startAndWaitForResult(loadJob); + auto archive = loadJob->archive(); QVERIFY(archive); if (!archive->isValid()) { @@ -86,7 +90,7 @@ QCOMPARE(archive->isReadOnly(), isReadOnly); QFETCH(bool, isSingleFolder); - QCOMPARE(archive->isSingleFolderArchive(), isSingleFolder); + QCOMPARE(archive->isSingleFolder(), isSingleFolder); QFETCH(Archive::EncryptionType, expectedEncryptionType); QCOMPARE(archive->encryptionType(), expectedEncryptionType); diff --git a/autotests/plugins/clirarplugin/clirartest.cpp b/autotests/plugins/clirarplugin/clirartest.cpp --- a/autotests/plugins/clirarplugin/clirartest.cpp +++ b/autotests/plugins/clirarplugin/clirartest.cpp @@ -25,15 +25,14 @@ */ #include "clirartest.h" -#include "kerfuffle/archiveentry.h" + +#include #include #include #include #include -#include - QTEST_GUILESS_MAIN(CliRarTest) using namespace Kerfuffle; @@ -73,7 +72,11 @@ } QFETCH(QString, archivePath); - Archive *archive = Archive::create(archivePath, m_plugin, this); + auto loadJob = Archive::load(archivePath, m_plugin, this); + QVERIFY(loadJob); + + TestHelper::startAndWaitForResult(loadJob); + auto archive = loadJob->archive(); QVERIFY(archive); if (!archive->isValid()) { @@ -87,7 +90,7 @@ QCOMPARE(archive->isReadOnly(), isReadOnly); QFETCH(bool, isSingleFolder); - QCOMPARE(archive->isSingleFolderArchive(), isSingleFolder); + QCOMPARE(archive->isSingleFolder(), isSingleFolder); QFETCH(Archive::EncryptionType, expectedEncryptionType); QCOMPARE(archive->encryptionType(), expectedEncryptionType); diff --git a/autotests/plugins/cliunarchiverplugin/cliunarchivertest.cpp b/autotests/plugins/cliunarchiverplugin/cliunarchivertest.cpp --- a/autotests/plugins/cliunarchiverplugin/cliunarchivertest.cpp +++ b/autotests/plugins/cliunarchiverplugin/cliunarchivertest.cpp @@ -18,17 +18,16 @@ */ #include "cliunarchivertest.h" -#include "jobs.h" -#include "kerfuffle/archiveentry.h" +#include "testhelper.h" + +#include #include #include #include #include #include -#include - QTEST_GUILESS_MAIN(CliUnarchiverTest) using namespace Kerfuffle; @@ -83,7 +82,11 @@ } QFETCH(QString, archivePath); - Archive *archive = Archive::create(archivePath, m_plugin, this); + auto loadJob = Archive::load(archivePath, m_plugin, this); + QVERIFY(loadJob); + + TestHelper::startAndWaitForResult(loadJob); + auto archive = loadJob->archive(); QVERIFY(archive); if (!archive->isValid()) { @@ -97,7 +100,7 @@ QCOMPARE(archive->isReadOnly(), isReadOnly); QFETCH(bool, isSingleFolder); - QCOMPARE(archive->isSingleFolderArchive(), isSingleFolder); + QCOMPARE(archive->isSingleFolder(), isSingleFolder); QFETCH(Archive::EncryptionType, expectedEncryptionType); QCOMPARE(archive->encryptionType(), expectedEncryptionType); @@ -293,7 +296,11 @@ } QFETCH(QString, archivePath); - Archive *archive = Archive::create(archivePath, m_plugin, this); + auto loadJob = Archive::load(archivePath, m_plugin, this); + QVERIFY(loadJob); + + TestHelper::startAndWaitForResult(loadJob); + auto archive = loadJob->archive(); QVERIFY(archive); if (!archive->isValid()) { diff --git a/autotests/testhelper/testhelper.cpp b/autotests/testhelper/testhelper.cpp --- a/autotests/testhelper/testhelper.cpp +++ b/autotests/testhelper/testhelper.cpp @@ -31,15 +31,15 @@ { QObject::connect(job, &KJob::result, &m_eventLoop, &QEventLoop::quit); job->start(); - m_eventLoop.exec(); + m_eventLoop.exec(); // krazy:exclude=crashy } QList TestHelper::getEntryList(Archive *archive) { QList list = QList(); - ListJob *listJob = archive->list(); - QObject::connect(listJob, &Job::newEntry, [&list](Archive::Entry* entry) { list << entry; }); - startAndWaitForResult(listJob); + auto loadJob = Archive::load(archive->fileName()); + QObject::connect(loadJob, &Job::newEntry, [&list](Archive::Entry* entry) { list << entry; }); + startAndWaitForResult(loadJob); return list; } diff --git a/kerfuffle/addtoarchive.cpp b/kerfuffle/addtoarchive.cpp --- a/kerfuffle/addtoarchive.cpp +++ b/kerfuffle/addtoarchive.cpp @@ -27,8 +27,9 @@ */ #include "addtoarchive.h" -#include "ark_debug.h" +#include "archiveentry.h" #include "archive_kerfuffle.h" +#include "ark_debug.h" #include "createdialog.h" #include "jobs.h" @@ -38,7 +39,6 @@ #include #include -#include #include #include #include @@ -48,7 +48,9 @@ namespace Kerfuffle { AddToArchive::AddToArchive(QObject *parent) - : KJob(parent), m_changeToFirstPath(false) + : KJob(parent) + , m_changeToFirstPath(false) + , m_enableHeaderEncryption(false) { } @@ -134,19 +136,13 @@ void AddToArchive::slotStartJob() { - Kerfuffle::CompressionOptions options; - if (m_entries.isEmpty()) { KMessageBox::error(NULL, i18n("No input files were given.")); emitResult(); return; } - Kerfuffle::Archive *archive; - if (!m_filename.isEmpty()) { - archive = Kerfuffle::Archive::create(m_filename, m_mimeType, this); - qCDebug(ARK) << "Set filename to " << m_filename; - } else { + if (m_filename.isEmpty()) { if (m_autoFilenameSuffix.isEmpty()) { KMessageBox::error(Q_NULLPTR, xi18n("You need to either supply a filename for the archive or a suffix (such as rar, tar.gz) with the --autofilename argument.")); emitResult(); @@ -171,32 +167,11 @@ finalName = base + QLatin1Char( '_' ) + QString::number(appendNumber) + QLatin1Char( '.' ) + m_autoFilenameSuffix; } - qCDebug(ARK) << "Autoset filename to "<< finalName; - archive = Kerfuffle::Archive::create(finalName, m_mimeType, this); - } - - Q_ASSERT(archive); - - if (!archive->isValid()) { - if (archive->error() == NoPlugin) { - KMessageBox::error(Q_NULLPTR, i18n("Failed to create the new archive. No suitable plugin found.")); - emitResult(); - return; - } - if (archive->error() == FailedPlugin) { - KMessageBox::error(Q_NULLPTR, i18n("Failed to create the new archive. Could not load a suitable plugin.")); - emitResult(); - return; - } - } else if (archive->isReadOnly()) { - KMessageBox::error(Q_NULLPTR, i18n("It is not possible to create archives of this type.")); - emitResult(); - return; + qCDebug(ARK) << "Autoset filename to" << finalName; + m_filename = finalName; } - if (!m_password.isEmpty()) { - archive->encrypt(m_password, m_enableHeaderEncryption); - } + Kerfuffle::CompressionOptions options; if (m_changeToFirstPath) { if (m_firstPath.isEmpty()) { @@ -215,22 +190,23 @@ qCDebug(ARK) << "Setting GlobalWorkDir to " << stripDir.path(); } - Kerfuffle::AddJob *job = - archive->addFiles(m_entries, new Archive::Entry(this), options); - - KIO::getJobTracker()->registerJob(job); + auto createJob = Archive::create(m_filename, m_mimeType, m_entries, options, this); - connect(job, &Kerfuffle::AddJob::result, this, &AddToArchive::slotFinished); + if (!m_password.isEmpty()) { + createJob->enableEncryption(m_password, m_enableHeaderEncryption); + } - job->start(); + KIO::getJobTracker()->registerJob(createJob); + connect(createJob, &KJob::result, this, &AddToArchive::slotFinished); + createJob->start(); } void AddToArchive::slotFinished(KJob *job) { qCDebug(ARK) << "AddToArchive job finished"; - if (job->error() && !job->errorText().isEmpty()) { - KMessageBox::error(Q_NULLPTR, job->errorText()); + if (job->error() && !job->errorString().isEmpty()) { + KMessageBox::error(Q_NULLPTR, job->errorString()); } emitResult(); diff --git a/kerfuffle/archive_kerfuffle.h b/kerfuffle/archive_kerfuffle.h --- a/kerfuffle/archive_kerfuffle.h +++ b/kerfuffle/archive_kerfuffle.h @@ -30,18 +30,18 @@ #include "kerfuffle_export.h" +#include +#include + #include #include -#include #include -#include - -class KJob; - namespace Kerfuffle { -class ListJob; +class LoadJob; +class BatchExtractJob; +class CreateJob; class ExtractJob; class DeleteJob; class AddJob; @@ -81,16 +81,17 @@ Q_PROPERTY(QString fileName READ fileName CONSTANT) Q_PROPERTY(QString comment READ comment CONSTANT) Q_PROPERTY(QMimeType mimeType READ mimeType CONSTANT) + Q_PROPERTY(bool isEmpty READ isEmpty) Q_PROPERTY(bool isReadOnly READ isReadOnly CONSTANT) - Q_PROPERTY(bool isSingleFolderArchive READ isSingleFolderArchive) + Q_PROPERTY(bool isSingleFolder MEMBER m_isSingleFolder READ isSingleFolder) Q_PROPERTY(bool isMultiVolume READ isMultiVolume WRITE setMultiVolume) Q_PROPERTY(bool numberOfVolumes READ numberOfVolumes) - Q_PROPERTY(EncryptionType encryptionType READ encryptionType) + Q_PROPERTY(EncryptionType encryptionType MEMBER m_encryptionType READ encryptionType) Q_PROPERTY(qulonglong numberOfFiles READ numberOfFiles) Q_PROPERTY(qulonglong numberOfFolders READ numberOfFolders) - Q_PROPERTY(qulonglong unpackedSize READ unpackedSize) + Q_PROPERTY(qulonglong unpackedSize MEMBER m_extractedFilesSize READ unpackedSize) Q_PROPERTY(qulonglong packedSize READ packedSize) - Q_PROPERTY(QString subfolderName READ subfolderName) + Q_PROPERTY(QString subfolderName MEMBER m_subfolderName READ subfolderName) Q_PROPERTY(QString password READ password) public: @@ -109,45 +110,67 @@ QString fileName() const; QString comment() const; QMimeType mimeType(); - bool isReadOnly(); - bool isSingleFolderArchive(); - bool isMultiVolume(); + bool isEmpty() const; + bool isReadOnly() const; + bool isSingleFolder() const; + bool isMultiVolume() const; void setMultiVolume(bool value); bool hasComment() const; int numberOfVolumes() const; - EncryptionType encryptionType(); + EncryptionType encryptionType() const; QString password() const; - qulonglong numberOfFiles(); - qulonglong numberOfFolders(); - qulonglong unpackedSize(); + qulonglong numberOfFiles() const; + qulonglong numberOfFolders() const; + qulonglong unpackedSize() const; qulonglong packedSize() const; - QString subfolderName(); + QString subfolderName() const; void setCompressionOptions(const CompressionOptions &opts); CompressionOptions compressionOptions() const; QString multiVolumeName() const; + ReadOnlyArchiveInterface *interface(); - static Archive *create(const QString &fileName, QObject *parent = 0); - static Archive *create(const QString &fileName, const QString &fixedMimeType, QObject *parent = 0); + /** + * @return Batch extraction job for @p filename to @p destination. + * @param autoSubfolder Whether the job will extract into a subfolder. + * @param preservePaths Whether the job will preserve paths. + * @param parent The parent for the archive. + */ + static BatchExtractJob *batchExtract(const QString &fileName, const QString &destination, bool autoSubfolder, bool preservePaths, QObject *parent = Q_NULLPTR); /** - * Create an archive instance from a given @p plugin. - * @param fileName The name of the archive. - * @return A valid archive if the plugin could be loaded, an invalid one otherwise (with the FailedPlugin error set). + * @return Job to create an archive for the given @p entries. + * @param fileName The name of the new archive. + * @param mimeType The mimetype of the new archive. */ - static Archive *create(const QString &fileName, Plugin *plugin, QObject *parent = Q_NULLPTR); + static CreateJob* create(const QString &fileName, const QString &mimeType, const QList &entries, const CompressionOptions& options, QObject *parent = Q_NULLPTR); - ~Archive(); + /** + * @return An empty archive with name @p fileName, mimetype @p mimeType and @p parent as parent. + */ + static Archive *createEmpty(const QString &fileName, const QString &mimeType, QObject *parent = Q_NULLPTR); - ArchiveError error() const; - bool isValid() const; + /** + * @return Job to load the archive @p fileName. + * @param parent The parent of the archive that will be loaded. + */ + static LoadJob* load(const QString &fileName, QObject *parent = Q_NULLPTR); - KJob* open(); - KJob* create(); + /** + * @return Job to load the archive @p fileName with mimetype @p mimeType. + * @param parent The parent of the archive that will be loaded. + */ + static LoadJob* load(const QString &fileName, const QString &mimeType, QObject *parent = Q_NULLPTR); /** - * @return A ListJob if the archive already exists. A null pointer otherwise. + * @return Job to load the archive @p fileName by using @p plugin. + * @param parent The parent of the archive that will be loaded. */ - ListJob* list(); + static LoadJob* load(const QString &fileName, Plugin *plugin, QObject *parent = Q_NULLPTR); + + ~Archive(); + + ArchiveError error() const; + bool isValid() const; DeleteJob* deleteFiles(QList &entries); CommentJob* addComment(const QString &comment); @@ -195,20 +218,27 @@ void encrypt(const QString &password, bool encryptHeader); private slots: - void onListFinished(KJob*); void onAddFinished(KJob*); void onUserQuery(Kerfuffle::Query*); void onNewEntry(const Archive::Entry *entry); private: Archive(ReadOnlyArchiveInterface *archiveInterface, bool isReadOnly, QObject *parent = 0); Archive(ArchiveError errorCode, QObject *parent = 0); - void listIfNotListed(); + static Archive *create(const QString &fileName, QObject *parent = 0); + static Archive *create(const QString &fileName, const QString &fixedMimeType, QObject *parent = 0); + + /** + * Create an archive instance from a given @p plugin. + * @param fileName The name of the archive. + * @return A valid archive if the plugin could be loaded, an invalid one otherwise (with the FailedPlugin error set). + */ + static Archive *create(const QString &fileName, Plugin *plugin, QObject *parent = Q_NULLPTR); + ReadOnlyArchiveInterface *m_iface; - bool m_hasBeenListed; bool m_isReadOnly; - bool m_isSingleFolderArchive; + bool m_isSingleFolder; bool m_isMultiVolume; QString m_subfolderName; diff --git a/kerfuffle/archive_kerfuffle.cpp b/kerfuffle/archive_kerfuffle.cpp --- a/kerfuffle/archive_kerfuffle.cpp +++ b/kerfuffle/archive_kerfuffle.cpp @@ -33,12 +33,12 @@ #include "mimetypes.h" #include "pluginmanager.h" -#include -#include - +#include #include #include +#include + namespace Kerfuffle { @@ -101,6 +101,51 @@ return new Archive(iface, !plugin->isReadWrite(), parent); } +BatchExtractJob *Archive::batchExtract(const QString &fileName, const QString &destination, bool autoSubfolder, bool preservePaths, QObject *parent) +{ + auto loadJob = load(fileName, parent); + auto batchJob = new BatchExtractJob(loadJob, destination, autoSubfolder, preservePaths); + + return batchJob; +} + +CreateJob *Archive::create(const QString &fileName, const QString &mimeType, const QList &entries, const CompressionOptions &options, QObject *parent) +{ + auto archive = create(fileName, mimeType, parent); + auto createJob = new CreateJob(archive, entries, options); + + return createJob; +} + +Archive *Archive::createEmpty(const QString &fileName, const QString &mimeType, QObject *parent) +{ + auto archive = create(fileName, mimeType, parent); + Q_ASSERT(archive->isEmpty()); + + return archive; +} + +LoadJob *Archive::load(const QString &fileName, QObject *parent) +{ + return load(fileName, QString(), parent); +} + +LoadJob *Archive::load(const QString &fileName, const QString &mimeType, QObject *parent) +{ + auto archive = create(fileName, mimeType, parent); + auto loadJob = new LoadJob(archive); + + return loadJob; +} + +LoadJob *Archive::load(const QString &fileName, Plugin *plugin, QObject *parent) +{ + auto archive = create(fileName, plugin, parent); + auto loadJob = new LoadJob(archive); + + return loadJob; +} + Archive::Archive(ArchiveError errorCode, QObject *parent) : QObject(parent) , m_iface(Q_NULLPTR) @@ -112,9 +157,8 @@ Archive::Archive(ReadOnlyArchiveInterface *archiveInterface, bool isReadOnly, QObject *parent) : QObject(parent) , m_iface(archiveInterface) - , m_hasBeenListed(false) , m_isReadOnly(isReadOnly) - , m_isSingleFolderArchive(false) + , m_isSingleFolder(false) , m_isMultiVolume(false) , m_extractedFilesSize(0) , m_error(NoError) @@ -124,8 +168,8 @@ { qCDebug(ARK) << "Created archive instance"; - Q_ASSERT(archiveInterface); - archiveInterface->setParent(this); + Q_ASSERT(m_iface); + m_iface->setParent(this); connect(m_iface, &ReadOnlyArchiveInterface::entry, this, &Archive::onNewEntry); } @@ -207,33 +251,37 @@ return m_mimeType; } -bool Archive::isReadOnly() +bool Archive::isEmpty() const +{ + return (numberOfFiles() == 0) && (numberOfFolders() == 0); +} + +bool Archive::isReadOnly() const { return isValid() ? (m_iface->isReadOnly() || m_isReadOnly || - (isMultiVolume() && (m_numberOfFiles > 0 || m_numberOfFolders > 0))) : false; + (isMultiVolume() && (numberOfFiles() > 0 || numberOfFolders() > 0))) : false; } -bool Archive::isSingleFolderArchive() +bool Archive::isSingleFolder() const { if (!isValid()) { return false; } - listIfNotListed(); - return m_isSingleFolderArchive; + return m_isSingleFolder; } bool Archive::hasComment() const { return isValid() ? !comment().isEmpty() : false; } -bool Archive::isMultiVolume() +bool Archive::isMultiVolume() const { if (!isValid()) { return false; } - listIfNotListed(); + return m_iface->isMultiVolume(); } @@ -247,63 +295,58 @@ return m_iface->numberOfVolumes(); } -Archive::EncryptionType Archive::encryptionType() +Archive::EncryptionType Archive::encryptionType() const { if (!isValid()) { return Unencrypted; } - listIfNotListed(); return m_encryptionType; } QString Archive::password() const { return m_iface->password(); } -qulonglong Archive::numberOfFiles() +qulonglong Archive::numberOfFiles() const { if (!isValid()) { return 0; } - listIfNotListed(); return m_numberOfFiles; } -qulonglong Archive::numberOfFolders() +qulonglong Archive::numberOfFolders() const { if (!isValid()) { return 0; } - listIfNotListed(); return m_numberOfFolders; } -qulonglong Archive::unpackedSize() +qulonglong Archive::unpackedSize() const { if (!isValid()) { return 0; } - listIfNotListed(); return m_extractedFilesSize; } qulonglong Archive::packedSize() const { return isValid() ? QFileInfo(fileName()).size() : 0; } -QString Archive::subfolderName() +QString Archive::subfolderName() const { if (!isValid()) { return QString(); } - listIfNotListed(); return m_subfolderName; } @@ -322,38 +365,6 @@ return m_error; } -KJob* Archive::open() -{ - return 0; -} - -KJob* Archive::create() -{ - return 0; -} - -ListJob* Archive::list() -{ - if (!isValid() || !QFileInfo::exists(fileName())) { - return Q_NULLPTR; - } - - qCDebug(ARK) << "Going to list files"; - - ListJob *job = new ListJob(m_iface); - - //if this job has not been listed before, we grab the opportunity to - //collect some information about the archive - if (!m_hasBeenListed) { - connect(job, &ListJob::result, this, &Archive::onListFinished); - } - - // FIXME: this is only a temporary workaround. See T3300 for a proper fix. - m_hasBeenListed = true; - - return job; -} - DeleteJob* Archive::deleteFiles(QList &entries) { if (!isValid()) { @@ -489,42 +500,8 @@ //folders/files other places than the root. //TODO: handle the case of creating a new file and singlefolderarchive //then. - if (m_isSingleFolderArchive && !job->error()) { - m_isSingleFolderArchive = false; - } -} - -void Archive::onListFinished(KJob* job) -{ - ListJob *ljob = qobject_cast(job); - m_extractedFilesSize = ljob->extractedFilesSize(); - m_isSingleFolderArchive = ljob->isSingleFolderArchive(); - m_subfolderName = ljob->subfolderName(); - if (m_subfolderName.isEmpty()) { - m_subfolderName = completeBaseName(); - } - - if (ljob->isPasswordProtected()) { - // If we already know the password, it means that the archive is header-encrypted. - m_encryptionType = m_iface->password().isEmpty() ? Encrypted : HeaderEncrypted; - } -} - -void Archive::listIfNotListed() -{ - if (!m_hasBeenListed) { - ListJob *job = list(); - if (!job) { - return; - } - - connect(job, &ListJob::userQuery, this, &Archive::onUserQuery); - - QEventLoop loop(this); - - connect(job, &KJob::result, &loop, &QEventLoop::quit); - job->start(); - loop.exec(); // krazy:exclude=crashy + if (m_isSingleFolder && !job->error()) { + m_isSingleFolder = false; } } @@ -548,4 +525,9 @@ return m_iface->multiVolumeName(); } +ReadOnlyArchiveInterface *Archive::interface() +{ + return m_iface; +} + } // namespace Kerfuffle diff --git a/kerfuffle/extractiondialog.h b/kerfuffle/extractiondialog.h --- a/kerfuffle/extractiondialog.h +++ b/kerfuffle/extractiondialog.h @@ -63,6 +63,8 @@ QString subfolder() const; public Q_SLOTS: + void setBusyGui(); + void setReadyGui(); void setSubfolder(const QString& subfolder); void setCurrentUrl(const QUrl &url); void restoreWindowSize(); diff --git a/kerfuffle/extractiondialog.cpp b/kerfuffle/extractiondialog.cpp --- a/kerfuffle/extractiondialog.cpp +++ b/kerfuffle/extractiondialog.cpp @@ -201,6 +201,21 @@ return m_ui->subfolder->text(); } +void ExtractionDialog::setBusyGui() +{ + QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); + fileWidget->setEnabled(false); + m_ui->setEnabled(false); + // TODO: tell the user why the dialog is busy (e.g. "archive is being loaded"). +} + +void ExtractionDialog::setReadyGui() +{ + QApplication::restoreOverrideCursor(); + fileWidget->setEnabled(true); + m_ui->setEnabled(true); +} + ExtractionDialog::~ExtractionDialog() { delete m_ui; diff --git a/kerfuffle/jobs.h b/kerfuffle/jobs.h --- a/kerfuffle/jobs.h +++ b/kerfuffle/jobs.h @@ -48,12 +48,21 @@ Q_OBJECT public: - void start(); + + /** + * @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(); + virtual bool doKill() Q_DECL_OVERRIDE; ReadOnlyArchiveInterface *archiveInterface(); QList m_archiveEntries; @@ -80,20 +89,38 @@ void userQuery(Kerfuffle::Query*); private: + Archive *m_archive; ReadOnlyArchiveInterface *m_archiveInterface; - QElapsedTimer jobTimer; class Private; Private * const d; }; -class KERFUFFLE_EXPORT ListJob : public Job +/** + * 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 ListJob(ReadOnlyArchiveInterface *interface); + explicit LoadJob(Archive *archive); + explicit LoadJob(ReadOnlyArchiveInterface *interface); qlonglong extractedFilesSize() const; bool isPasswordProtected() const; @@ -103,7 +130,12 @@ 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; @@ -116,6 +148,66 @@ 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 slotLoadingFinished(KJob *job); + +private: + void setupDestination(); + + LoadJob *m_loadJob; + QString m_destination; + bool m_autoSubfolder; + bool m_preservePaths; +}; + +/** + * Create a new archive given a bunch of entries. + */ +class KERFUFFLE_EXPORT CreateJob : public Job +{ + Q_OBJECT + +public: + explicit CreateJob(Archive *archive, const QList &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: + QList m_entries; + CompressionOptions m_options; +}; + class KERFUFFLE_EXPORT ExtractJob : public Job { Q_OBJECT diff --git a/kerfuffle/jobs.cpp b/kerfuffle/jobs.cpp --- a/kerfuffle/jobs.cpp +++ b/kerfuffle/jobs.cpp @@ -36,7 +36,9 @@ #include #include #include +#include +#include #include namespace Kerfuffle @@ -62,14 +64,23 @@ q->doWork(); } -Job::Job(ReadOnlyArchiveInterface *interface) +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); @@ -84,13 +95,51 @@ 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); @@ -151,6 +200,10 @@ { qCDebug(ARK) << "Job finished, result:" << result << ", time:" << jobTimer.elapsed() << "ms"; + if (archive() && !archive()->isValid()) { + setError(KJob::UserDefinedError); + } + emitResult(); } @@ -173,49 +226,73 @@ return ret; } -ListJob::ListJob(ReadOnlyArchiveInterface *interface) - : Job(interface) +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) << "ListJob started"; - connect(this, &ListJob::newEntry, this, &ListJob::onNewEntry); + qCDebug(ARK) << "LoadJob started"; + connect(this, &LoadJob::newEntry, this, &LoadJob::onNewEntry); } -void ListJob::doWork() +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...")); connectToArchiveInterfaceSignals(); + bool ret = archiveInterface()->list(); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } -qlonglong ListJob::extractedFilesSize() const +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 ListJob::isPasswordProtected() const +bool LoadJob::isPasswordProtected() const { return m_isPasswordProtected; } -bool ListJob::isSingleFolderArchive() const +bool LoadJob::isSingleFolderArchive() const { if (m_filesCount == 1 && m_dirCount == 0) { return false; } return m_isSingleFolderArchive; } -void ListJob::onNewEntry(const Archive::Entry *entry) +void LoadJob::onNewEntry(const Archive::Entry *entry) { m_extractedFilesSize += entry->property("size").toLongLong(); m_isPasswordProtected |= entry->property("isPasswordProtected").toBool(); @@ -243,15 +320,114 @@ } } -QString ListJob::subfolderName() const +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); + + // Forward signals + connect(m_loadJob, &Kerfuffle::Job::newEntry, this, &BatchExtractJob::newEntry); + connect(m_loadJob, &Kerfuffle::Job::userQuery, this, &BatchExtractJob::userQuery); + m_loadJob->start(); +} + +void BatchExtractJob::slotLoadingFinished(KJob *job) +{ + if (job->error()) { + emitResult(); + return; + } + + // Now we can start extraction. + setupDestination(); + + Kerfuffle::ExtractionOptions options; + options[QStringLiteral("PreservePaths")] = 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); + 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 QList &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); + addJob->start(); + } else { + emitResult(); + } +} + ExtractJob::ExtractJob(const QList &entries, const QString &destinationDir, const ExtractionOptions &options, ReadOnlyArchiveInterface *interface) : Job(interface) , m_entries(entries) diff --git a/part/archivemodel.h b/part/archivemodel.h --- a/part/archivemodel.h +++ b/part/archivemodel.h @@ -23,12 +23,12 @@ #ifndef ARCHIVEMODEL_H #define ARCHIVEMODEL_H -#include -#include +#include "archiveentry.h" #include -#include -#include "kerfuffle/archiveentry.h" + +#include +#include using Kerfuffle::Archive; @@ -62,7 +62,9 @@ QMimeData *mimeData(const QModelIndexList & indexes) const Q_DECL_OVERRIDE; bool dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) Q_DECL_OVERRIDE; - KJob* setArchive(Kerfuffle::Archive *archive); + void reset(); + void createEmptyArchive(const QString &path, const QString &mimeType, QObject *parent); + KJob* loadArchive(const QString &path, const QString &mimeType, QObject *parent); Kerfuffle::Archive *archive() const; Archive::Entry *entryForIndex(const QModelIndex &index); diff --git a/part/archivemodel.cpp b/part/archivemodel.cpp --- a/part/archivemodel.cpp +++ b/part/archivemodel.cpp @@ -23,7 +23,7 @@ */ #include "archivemodel.h" -#include "kerfuffle/jobs.h" +#include "jobs.h" #include #include @@ -693,8 +693,11 @@ void ArchiveModel::slotLoadingFinished(KJob *job) { if (!job->error()) { + + m_archive.reset(qobject_cast(job)->archive()); QElapsedTimer timer; timer.start(); + int i = 0; foreach(Archive::Entry *entry, m_newArchiveEntries) { newEntry(entry, DoNotNotifyViews); @@ -741,33 +744,38 @@ return m_archive.data(); } -KJob* ArchiveModel::setArchive(Kerfuffle::Archive *archive) +void ArchiveModel::reset() { - m_archive.reset(archive); - + m_archive.reset(Q_NULLPTR); m_rootEntry.clear(); s_previousMatch = Q_NULLPTR; s_previousPieces->clear(); - - Kerfuffle::ListJob *job = Q_NULLPTR; - m_newArchiveEntries.clear(); - if (m_archive) { - job = m_archive->list(); // TODO: call "open" or "create"? - if (job) { - connect(job, &Kerfuffle::ListJob::newEntry, this, &ArchiveModel::slotNewEntryFromSetArchive); - connect(job, &Kerfuffle::ListJob::result, this, &ArchiveModel::slotLoadingFinished); - connect(job, &Kerfuffle::ListJob::userQuery, this, &ArchiveModel::slotUserQuery); - emit loadingStarted(); - - // TODO: make sure if it's ok to not have calls to beginRemoveColumns here - m_showColumns.clear(); - } - } + // TODO: make sure if it's ok to not have calls to beginRemoveColumns here + m_showColumns.clear(); beginResetModel(); endResetModel(); - return job; +} + +void ArchiveModel::createEmptyArchive(const QString &path, const QString &mimeType, QObject *parent) +{ + reset(); + m_archive.reset(Archive::createEmpty(path, mimeType, parent)); +} + +KJob *ArchiveModel::loadArchive(const QString &path, const QString &mimeType, QObject *parent) +{ + reset(); + + auto loadJob = Archive::load(path, mimeType, parent); + connect(loadJob, &KJob::result, this, &ArchiveModel::slotLoadingFinished); + connect(loadJob, &Job::newEntry, this, &ArchiveModel::slotNewEntryFromSetArchive); + connect(loadJob, &Job::userQuery, this, &ArchiveModel::slotUserQuery); + + emit loadingStarted(); + + return loadJob; } ExtractJob* ArchiveModel::extractFile(Archive::Entry *file, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options) const diff --git a/part/part.h b/part/part.h --- a/part/part.h +++ b/part/part.h @@ -164,6 +164,8 @@ void quit(); private: + void createArchive(); + void loadArchive(); void resetGui(); void setupView(); void setupActions(); diff --git a/part/part.cpp b/part/part.cpp --- a/part/part.cpp +++ b/part/part.cpp @@ -568,11 +568,44 @@ job->start(); } +void Part::createArchive() +{ + const QString fixedMimeType = arguments().metaData()[QStringLiteral("fixedMimeType")]; + m_model->createEmptyArchive(localFilePath(), fixedMimeType, m_model); + + if (arguments().metaData().contains(QStringLiteral("volumeSize"))) { + m_model->archive()->setMultiVolume(true); + } + + const QString password = arguments().metaData()[QStringLiteral("encryptionPassword")]; + if (!password.isEmpty()) { + m_model->encryptArchive(password, + arguments().metaData()[QStringLiteral("encryptHeader")] == QLatin1String("true")); + } + + updateActions(); + m_view->setDropsEnabled(true); +} + +void Part::loadArchive() +{ + const QString fixedMimeType = arguments().metaData()[QStringLiteral("fixedMimeType")]; + auto job = m_model->loadArchive(localFilePath(), fixedMimeType, m_model); + + if (job) { + registerJob(job); + job->start(); + } else { + updateActions(); + } +} + void Part::resetGui() { m_messageWidget->hide(); m_commentView->clear(); m_commentBox->hide(); + m_infoPanel->setIndex(QModelIndex()); } void Part::slotTestingDone(KJob* job) @@ -687,46 +720,12 @@ return false; } - const QString fixedMimeType = arguments().metaData()[QStringLiteral("fixedMimeType")]; - QScopedPointer archive(Kerfuffle::Archive::create(localFilePath(), fixedMimeType, m_model)); - Q_ASSERT(archive); - - if (archive->error() == NoPlugin) { - displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Ark was not able to open %1. No suitable plugin found." - "Ark does not seem to support this file type.", - QFileInfo(localFilePath()).fileName())); - return false; - } - - if (archive->error() == FailedPlugin) { - displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Ark was not able to open %1. Failed to load a suitable plugin." - "Make sure any executables needed to handle the archive type are installed.", - QFileInfo(localFilePath()).fileName())); - return false; - } - - Q_ASSERT(archive->isValid()); - - if (arguments().metaData().contains(QStringLiteral("volumeSize"))) { - archive.data()->setMultiVolume(true); - } + const bool creatingNewArchive = arguments().metaData()[QStringLiteral("createNewArchive")] == QLatin1String("true"); - // Plugin loaded successfully. - KJob *job = m_model->setArchive(archive.take()); - if (job) { - registerJob(job); - job->start(); + if (creatingNewArchive) { + createArchive(); } else { - updateActions(); - m_view->setDropsEnabled(true); - } - - m_infoPanel->setIndex(QModelIndex()); - - const QString password = arguments().metaData()[QStringLiteral("encryptionPassword")]; - if (!password.isEmpty()) { - m_model->encryptArchive(password, - arguments().metaData()[QStringLiteral("encryptHeader")] == QLatin1String("true")); + loadArchive(); } return true; @@ -828,12 +827,11 @@ if (job->error() != KJob::KilledJobError) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Loading the archive %1 failed with the following error:%2", localFilePath(), - job->errorText())); + job->errorString())); } // The file failed to open, so reset the open archive, info panel and caption. - m_model->setArchive(Q_NULLPTR); - + m_model->reset(); m_infoPanel->setPrettyFileName(QString()); m_infoPanel->updateWithDefaults(); @@ -1063,7 +1061,7 @@ bool Part::isSingleFolderArchive() const { - return m_model->archive()->isSingleFolderArchive(); + return m_model->archive()->isSingleFolder(); } QString Part::detectSubfolder() const