diff --git a/autotests/kerfuffle/jobstest.cpp b/autotests/kerfuffle/jobstest.cpp index b78dba89..2807eebb 100644 --- a/autotests/kerfuffle/jobstest.cpp +++ b/autotests/kerfuffle/jobstest.cpp @@ -1,386 +1,385 @@ /* * Copyright (c) 2010-2011 Raphael Kubo da Costa * 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 "jsonarchiveinterface.h" #include "kerfuffle/jobs.h" #include "kerfuffle/archiveentry.h" #include #include #include using namespace Kerfuffle; class JobsTest : public QObject { Q_OBJECT public: JobsTest(); protected Q_SLOTS: void init(); void slotNewEntry(Archive::Entry *entry); private Q_SLOTS: // ListJob-related tests void testListJob_data(); void testListJob(); // ExtractJob-related tests void testExtractJobAccessors(); void testTempExtractJob(); // DeleteJob-related tests void testRemoveEntries_data(); void testRemoveEntries(); // AddJob-related tests void testAddEntries_data(); void testAddEntries(); private: JSONArchiveInterface *createArchiveInterface(const QString& filePath); QList listEntries(JSONArchiveInterface *iface); void startAndWaitForResult(KJob *job); QList m_entries; QEventLoop m_eventLoop; }; QTEST_GUILESS_MAIN(JobsTest) JobsTest::JobsTest() : QObject(Q_NULLPTR) , m_eventLoop(this) { } void JobsTest::init() { m_entries.clear(); } void JobsTest::slotNewEntry(Archive::Entry *entry) { m_entries.append(entry); } JSONArchiveInterface *JobsTest::createArchiveInterface(const QString& filePath) { JSONArchiveInterface *iface = new JSONArchiveInterface(this, {filePath}); if (!iface->open()) { qDebug() << "Could not open" << filePath; return Q_NULLPTR; } return iface; } QList JobsTest::listEntries(JSONArchiveInterface *iface) { m_entries.clear(); ListJob *listJob = new ListJob(iface); connect(listJob, &Job::newEntry, this, &JobsTest::slotNewEntry); startAndWaitForResult(listJob); return m_entries; } void JobsTest::startAndWaitForResult(KJob *job) { connect(job, &KJob::result, &m_eventLoop, &QEventLoop::quit); job->start(); m_eventLoop.exec(); } void JobsTest::testListJob_data() { QTest::addColumn("jsonArchive"); QTest::addColumn("expectedExtractedFilesSize"); QTest::addColumn("isPasswordProtected"); QTest::addColumn("isSingleFolder"); QTest::addColumn("expectedEntryNames"); QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << 0LL << false << false << QStringList {QStringLiteral("a.txt"), QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt")}; QTest::newRow("archive002.json") << QFINDTESTDATA("data/archive002.json") << 45959LL << false << false << QStringList {QStringLiteral("a.txt"), QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt")}; QTest::newRow("archive-deepsinglehierarchy.json") << QFINDTESTDATA("data/archive-deepsinglehierarchy.json") << 0LL << false << true << QStringList { // Depth-first order! QStringLiteral("aDir/"), QStringLiteral("aDir/aDirInside/"), QStringLiteral("aDir/aDirInside/anotherDir/"), QStringLiteral("aDir/aDirInside/anotherDir/file.txt"), QStringLiteral("aDir/b.txt") }; QTest::newRow("archive-multiplefolders.json") << QFINDTESTDATA("data/archive-multiplefolders.json") << 0LL << false << false << QStringList {QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt"), QStringLiteral("anotherDir/"), QStringLiteral("anotherDir/file.txt")}; QTest::newRow("archive-nodir-manyfiles.json") << QFINDTESTDATA("data/archive-nodir-manyfiles.json") << 0LL << false << false << QStringList {QStringLiteral("a.txt"), QStringLiteral("file.txt")}; QTest::newRow("archive-onetopfolder.json") << QFINDTESTDATA("data/archive-onetopfolder.json") << 0LL << false << true << QStringList {QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt")}; QTest::newRow("archive-password.json") << QFINDTESTDATA("data/archive-password.json") << 0LL << true << false // Possibly unexpected behavior of listing: // 1. Directories are listed before files, if they are empty! // 2. Files are sorted alphabetically. << QStringList {QStringLiteral("aDirectory/"), QStringLiteral("bar.txt"), QStringLiteral("foo.txt")}; QTest::newRow("archive-singlefile.json") << QFINDTESTDATA("data/archive-singlefile.json") << 0LL << false << false << QStringList {QStringLiteral("a.txt")}; QTest::newRow("archive-emptysinglefolder.json") << QFINDTESTDATA("data/archive-emptysinglefolder.json") << 0LL << false << true << QStringList {QStringLiteral("aDir/")}; QTest::newRow("archive-unorderedsinglefolder.json") << QFINDTESTDATA("data/archive-unorderedsinglefolder.json") << 0LL << false << true << QStringList { QStringLiteral("aDir/"), QStringLiteral("aDir/anotherDir/"), QStringLiteral("aDir/anotherDir/bar.txt"), QStringLiteral("aDir/foo.txt") }; } void JobsTest::testListJob() { QFETCH(QString, jsonArchive); JSONArchiveInterface *iface = createArchiveInterface(jsonArchive); QVERIFY(iface); ListJob *listJob = new ListJob(iface); listJob->setAutoDelete(false); startAndWaitForResult(listJob); QFETCH(qlonglong, expectedExtractedFilesSize); QCOMPARE(listJob->extractedFilesSize(), expectedExtractedFilesSize); QFETCH(bool, isPasswordProtected); QCOMPARE(listJob->isPasswordProtected(), isPasswordProtected); QFETCH(bool, isSingleFolder); QCOMPARE(listJob->isSingleFolderArchive(), isSingleFolder); QFETCH(QStringList, expectedEntryNames); auto archiveEntries = listEntries(iface); QCOMPARE(archiveEntries.size(), expectedEntryNames.size()); for (int i = 0; i < archiveEntries.size(); i++) { QCOMPARE(archiveEntries.at(i)->property("fullPath").toString(), expectedEntryNames.at(i)); } listJob->deleteLater(); } void JobsTest::testExtractJobAccessors() { JSONArchiveInterface *iface = createArchiveInterface(QFINDTESTDATA("data/archive001.json")); ExtractJob *job = new ExtractJob(QList(), QStringLiteral("/tmp/some-dir"), ExtractionOptions(), iface); ExtractionOptions defaultOptions; defaultOptions[QStringLiteral("PreservePaths")] = false; QCOMPARE(job->destinationDirectory(), QLatin1String("/tmp/some-dir")); QCOMPARE(job->extractionOptions(), defaultOptions); job->setAutoDelete(false); startAndWaitForResult(job); QCOMPARE(job->destinationDirectory(), QLatin1String("/tmp/some-dir")); QCOMPARE(job->extractionOptions(), defaultOptions); delete job; ExtractionOptions options; options[QStringLiteral("PreservePaths")] = true; options[QStringLiteral("foo")] = QLatin1String("bar"); options[QStringLiteral("pi")] = 3.14f; job = new ExtractJob(QList(), QStringLiteral("/root"), options, iface); QCOMPARE(job->destinationDirectory(), QLatin1String("/root")); QCOMPARE(job->extractionOptions(), options); job->setAutoDelete(false); startAndWaitForResult(job); QCOMPARE(job->destinationDirectory(), QLatin1String("/root")); QCOMPARE(job->extractionOptions(), options); delete job; } void JobsTest::testTempExtractJob() { JSONArchiveInterface *iface = createArchiveInterface(QFINDTESTDATA("data/archive-malicious.json")); PreviewJob *job = new PreviewJob(new Archive::Entry(this, QStringLiteral("anotherDir/../../file.txt")), false, iface); QVERIFY(job->validatedFilePath().endsWith(QLatin1String("anotherDir/file.txt"))); QVERIFY(job->extractionOptions()[QStringLiteral("PreservePaths")].toBool()); job->setAutoDelete(false); startAndWaitForResult(job); QVERIFY(job->validatedFilePath().endsWith(QLatin1String("anotherDir/file.txt"))); QVERIFY(job->extractionOptions()[QStringLiteral("PreservePaths")].toBool()); delete job; } void JobsTest::testRemoveEntries_data() { QTest::addColumn("jsonArchive"); QTest::addColumn>("entries"); QTest::addColumn>("entriesToDelete"); QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QList { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("aDir/")), new Archive::Entry(this, QStringLiteral("aDir/b.txt")), new Archive::Entry(this, QStringLiteral("c.txt")) } << QList {new Archive::Entry(this, QStringLiteral("c.txt"))}; QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QList { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("aDir/")), new Archive::Entry(this, QStringLiteral("aDir/b.txt")), new Archive::Entry(this, QStringLiteral("c.txt")) } << QList { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("c.txt")) }; // Error test: if we delete non-existent entries, the archive must not change. QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QList { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("aDir/")), new Archive::Entry(this, QStringLiteral("aDir/b.txt")), new Archive::Entry(this, QStringLiteral("c.txt")) } << QList {new Archive::Entry(this, QStringLiteral("foo.txt"))}; } void JobsTest::testRemoveEntries() { QFETCH(QString, jsonArchive); JSONArchiveInterface *iface = createArchiveInterface(jsonArchive); QVERIFY(iface); QFETCH(QList, entries); QFETCH(QList, entriesToDelete); QList expectedRemainingEntries; Q_FOREACH (Archive::Entry *entry, entries) { if (!entriesToDelete.contains(entry)) { expectedRemainingEntries.append(entry); } } DeleteJob *deleteJob = new DeleteJob(entriesToDelete, iface); startAndWaitForResult(deleteJob); auto remainingEntries = listEntries(iface); QCOMPARE(remainingEntries.size(), expectedRemainingEntries.size()); for (int i = 0; i < remainingEntries.size(); i++) { QCOMPARE(remainingEntries.at(i), expectedRemainingEntries.at(i)); } iface->deleteLater(); } void JobsTest::testAddEntries_data() { QTest::addColumn("jsonArchive"); QTest::addColumn("originalEntries"); QTest::addColumn("entriesToAdd"); QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QStringList {QStringLiteral("a.txt"), QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt")} << QStringList {QStringLiteral("foo.txt")}; QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QStringList {QStringLiteral("a.txt"), QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt")} << QStringList {QStringLiteral("foo.txt"), QStringLiteral("bar.txt")}; // Error test: if we add an already existent entry, the archive must not change. QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QStringList {QStringLiteral("a.txt"), QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt")} << QStringList {QStringLiteral("c.txt")}; } void JobsTest::testAddEntries() { QFETCH(QString, jsonArchive); JSONArchiveInterface *iface = createArchiveInterface(jsonArchive); QVERIFY(iface); QFETCH(QList, originalEntries); auto currentEntries = listEntries(iface); QCOMPARE(currentEntries.size(), originalEntries.size()); QFETCH(QList, entriesToAdd); - const Archive::Entry rootEntry; - AddJob *addJob = new AddJob(entriesToAdd, &rootEntry, CompressionOptions(), iface); + AddJob *addJob = new AddJob(entriesToAdd, CompressionOptions(), iface); startAndWaitForResult(addJob); currentEntries = listEntries(iface); int expectedEntriesCount = originalEntries.size(); Q_FOREACH (Archive::Entry *entry, entriesToAdd) { if (!originalEntries.contains(entry)) { expectedEntriesCount++; } } QCOMPARE(currentEntries.size(), expectedEntriesCount); iface->deleteLater(); } #include "jobstest.moc" diff --git a/kerfuffle/addtoarchive.cpp b/kerfuffle/addtoarchive.cpp index 4ebed744..49c0aeaf 100644 --- a/kerfuffle/addtoarchive.cpp +++ b/kerfuffle/addtoarchive.cpp @@ -1,271 +1,271 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2008 Harald Hvaal * Copyright (C) 2009 Raphael Kubo da Costa * * 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 "addtoarchive.h" #include "ark_debug.h" #include "archive_kerfuffle.h" #include "createdialog.h" #include "jobs.h" #include #include #include #include #include #include #include #include #include #include #include namespace Kerfuffle { AddToArchive::AddToArchive(QObject *parent) : KJob(parent), m_changeToFirstPath(false) { } AddToArchive::~AddToArchive() { } void AddToArchive::setAutoFilenameSuffix(const QString& suffix) { m_autoFilenameSuffix = suffix; } void AddToArchive::setChangeToFirstPath(bool value) { m_changeToFirstPath = value; } void AddToArchive::setFilename(const QUrl &path) { m_filename = path.toDisplayString(QUrl::PreferLocalFile); } void AddToArchive::setMimeType(const QString & mimeType) { m_mimeType = mimeType; } void AddToArchive::setPassword(const QString &password) { m_password = password; } void AddToArchive::setHeaderEncryptionEnabled(bool enabled) { m_enableHeaderEncryption = enabled; } bool AddToArchive::showAddDialog() { qCDebug(ARK) << "Opening add dialog"; QPointer dialog = new Kerfuffle::CreateDialog( Q_NULLPTR, // parent i18n("Compress to Archive"), // caption QUrl::fromLocalFile(m_firstPath)); // startDir bool ret = dialog.data()->exec(); if (ret) { qCDebug(ARK) << "CreateDialog returned URL:" << dialog.data()->selectedUrl().toString(); qCDebug(ARK) << "CreateDialog returned mime:" << dialog.data()->currentMimeType().name(); setFilename(dialog.data()->selectedUrl()); setMimeType(dialog.data()->currentMimeType().name()); setPassword(dialog.data()->password()); setHeaderEncryptionEnabled(dialog.data()->isHeaderEncryptionEnabled()); } delete dialog.data(); return ret; } bool AddToArchive::addInput(const QUrl &url) { Archive::Entry *entry = new Archive::Entry(); entry->setFullPath(url.toDisplayString(QUrl::PreferLocalFile)); m_entries << entry; if (m_firstPath.isEmpty()) { QString firstEntry = url.toDisplayString(QUrl::PreferLocalFile); m_firstPath = QFileInfo(firstEntry).dir().absolutePath(); } return true; } void AddToArchive::start() { qCDebug(ARK) << "Starting job"; QTimer::singleShot(0, this, &AddToArchive::slotStartJob); } 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_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(); return; } if (m_firstPath.isEmpty()) { qCWarning(ARK) << "Weird, this should not happen. no firstpath defined. aborting"; emitResult(); return; } const QString base = detectBaseName(m_entries); QString finalName = base + QLatin1Char( '.' ) + m_autoFilenameSuffix; //if file already exists, append a number to the base until it doesn't //exist int appendNumber = 0; while (QFileInfo::exists(finalName)) { ++appendNumber; 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; } if (!m_password.isEmpty()) { archive->encrypt(m_password, m_enableHeaderEncryption); } if (m_changeToFirstPath) { if (m_firstPath.isEmpty()) { qCWarning(ARK) << "Weird, this should not happen. no firstpath defined. aborting"; emitResult(); return; } const QDir stripDir(m_firstPath); foreach (Archive::Entry *entry, m_entries) { entry->setFullPath(stripDir.absoluteFilePath(entry->property("fullPath").toString())); } options[QStringLiteral( "GlobalWorkDir" )] = stripDir.path(); qCDebug(ARK) << "Setting GlobalWorkDir to " << stripDir.path(); } Kerfuffle::AddJob *job = - archive->addFiles(m_entries, Q_NULLPTR, options); + archive->addFiles(m_entries, options); KIO::getJobTracker()->registerJob(job); connect(job, &Kerfuffle::AddJob::result, this, &AddToArchive::slotFinished); job->start(); } void AddToArchive::slotFinished(KJob *job) { qCDebug(ARK) << "AddToArchive job finished"; if (job->error() && !job->errorText().isEmpty()) { KMessageBox::error(Q_NULLPTR, job->errorText()); } emitResult(); } QString AddToArchive::detectBaseName(const QList &entries) const { QFileInfo fileInfo = QFileInfo(entries.first()->property("fullPath").toString()); QDir parentDir = fileInfo.dir(); QString base = parentDir.absolutePath() + QLatin1Char('/'); if (entries.size() > 1) { if (!parentDir.isRoot()) { // Use directory name for the new archive. base += parentDir.dirName(); } } else { // Strip filename of its extension, but only if present (see #362690). if (!QMimeDatabase().mimeTypeForFile(fileInfo.fileName(), QMimeDatabase::MatchExtension).isDefault()) { base += fileInfo.completeBaseName(); } else { base += fileInfo.fileName(); } } // Special case for compressed tar archives. if (base.right(4).toUpper() == QLatin1String(".TAR")) { base.chop(4); } if (base.endsWith(QLatin1Char('/'))) { base.chop(1); } return base; } } diff --git a/kerfuffle/archive_kerfuffle.cpp b/kerfuffle/archive_kerfuffle.cpp index 92394dfe..14cca8bb 100644 --- a/kerfuffle/archive_kerfuffle.cpp +++ b/kerfuffle/archive_kerfuffle.cpp @@ -1,448 +1,448 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008 Harald Hvaal * Copyright (c) 2009-2011 Raphael Kubo da Costa * * 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 "archive_kerfuffle.h" #include "archiveentry.h" #include "archiveinterface.h" #include "jobs.h" #include "mimetypes.h" #include "pluginmanager.h" #include #include #include namespace Kerfuffle { Archive *Archive::create(const QString &fileName, QObject *parent) { return create(fileName, QString(), parent); } Archive *Archive::create(const QString &fileName, const QString &fixedMimeType, QObject *parent) { qCDebug(ARK) << "Going to create archive" << fileName; PluginManager pluginManager; const QMimeType mimeType = fixedMimeType.isEmpty() ? determineMimeType(fileName) : QMimeDatabase().mimeTypeForName(fixedMimeType); const QVector offers = pluginManager.preferredPluginsFor(mimeType); if (offers.isEmpty()) { qCCritical(ARK) << "Could not find a plugin to handle" << fileName; return new Archive(NoPlugin, parent); } Archive *archive; foreach (Plugin *plugin, offers) { archive = create(fileName, plugin, parent); // Use the first valid plugin, according to the priority sorting. if (archive->isValid()) { return archive; } } qCCritical(ARK) << "Failed to find a usable plugin for" << fileName; return archive; } Archive *Archive::create(const QString &fileName, Plugin *plugin, QObject *parent) { Q_ASSERT(plugin); qCDebug(ARK) << "Checking plugin" << plugin->metaData().pluginId(); KPluginFactory *factory = KPluginLoader(plugin->metaData().fileName()).factory(); if (!factory) { qCWarning(ARK) << "Invalid plugin factory for" << plugin->metaData().pluginId(); return new Archive(FailedPlugin, parent); } const QVariantList args = {QVariant(QFileInfo(fileName).absoluteFilePath())}; ReadOnlyArchiveInterface *iface = factory->create(Q_NULLPTR, args); if (!iface) { qCWarning(ARK) << "Could not create plugin instance" << plugin->metaData().pluginId(); return new Archive(FailedPlugin, parent); } if (!plugin->isValid()) { qCDebug(ARK) << "Cannot use plugin" << plugin->metaData().pluginId() << "- check whether" << plugin->readOnlyExecutables() << "are installed."; return new Archive(FailedPlugin, parent); } qCDebug(ARK) << "Successfully loaded plugin" << plugin->metaData().pluginId(); return new Archive(iface, !plugin->isReadWrite(), parent); } Archive::Archive(ArchiveError errorCode, QObject *parent) : QObject(parent) , m_iface(Q_NULLPTR) , m_error(errorCode) { qCDebug(ARK) << "Created archive instance with error"; } Archive::Archive(ReadOnlyArchiveInterface *archiveInterface, bool isReadOnly, QObject *parent) : QObject(parent) , m_iface(archiveInterface) , m_hasBeenListed(false) , m_isReadOnly(isReadOnly) , m_isSingleFolderArchive(false) , m_extractedFilesSize(0) , m_error(NoError) , m_encryptionType(Unencrypted) , m_numberOfFiles(0) { qCDebug(ARK) << "Created archive instance"; Q_ASSERT(archiveInterface); archiveInterface->setParent(this); connect(m_iface, &ReadOnlyArchiveInterface::entry, this, &Archive::onNewEntry); } Archive::~Archive() { } QString Archive::completeBaseName() const { QString base = QFileInfo(fileName()).completeBaseName(); // Special case for compressed tar archives. if (base.right(4).toUpper() == QLatin1String(".TAR")) { base.chop(4); } return base; } QString Archive::fileName() const { return isValid() ? m_iface->filename() : QString(); } QString Archive::comment() const { return isValid() ? m_iface->comment() : QString(); } CommentJob* Archive::addComment(const QString &comment) { if (!isValid()) { return Q_NULLPTR; } qCDebug(ARK) << "Going to add comment:" << comment; Q_ASSERT(!isReadOnly()); CommentJob *job = new CommentJob(comment, static_cast(m_iface)); return job; } TestJob* Archive::testArchive() { if (!isValid()) { return Q_NULLPTR; } qCDebug(ARK) << "Going to test archive"; TestJob *job = new TestJob(m_iface); return job; } QMimeType Archive::mimeType() { if (!isValid()) { return QMimeType(); } if (!m_mimeType.isValid()) { m_mimeType = determineMimeType(fileName()); } return m_mimeType; } bool Archive::isReadOnly() const { return isValid() ? (m_iface->isReadOnly() || m_isReadOnly) : false; } bool Archive::isSingleFolderArchive() { if (!isValid()) { return false; } listIfNotListed(); return m_isSingleFolderArchive; } bool Archive::hasComment() const { return isValid() ? !comment().isEmpty() : false; } Archive::EncryptionType Archive::encryptionType() { if (!isValid()) { return Unencrypted; } listIfNotListed(); return m_encryptionType; } qulonglong Archive::numberOfFiles() { if (!isValid()) { return 0; } listIfNotListed(); return m_numberOfFiles; } qulonglong Archive::unpackedSize() { if (!isValid()) { return 0; } listIfNotListed(); return m_extractedFilesSize; } qulonglong Archive::packedSize() const { return isValid() ? QFileInfo(fileName()).size() : 0; } QString Archive::subfolderName() { if (!isValid()) { return QString(); } listIfNotListed(); return m_subfolderName; } void Archive::onNewEntry(const Archive::Entry *entry) { if (!entry->isDir()) { m_numberOfFiles++; } } bool Archive::isValid() const { return m_iface && (m_error == NoError); } ArchiveError Archive::error() const { 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); } return job; } DeleteJob* Archive::deleteFiles(QList &entries) { if (!isValid()) { return Q_NULLPTR; } qCDebug(ARK) << "Going to delete entries" << entries; if (m_iface->isReadOnly()) { return 0; } DeleteJob *newJob = new DeleteJob(entries, static_cast(m_iface)); return newJob; } -AddJob* Archive::addFiles(QList &files, const Archive::Entry *destination, const CompressionOptions& options) +AddJob* Archive::addFiles(QList &files, const CompressionOptions& options) { if (!isValid()) { return Q_NULLPTR; } CompressionOptions newOptions = options; if (encryptionType() != Unencrypted) { newOptions[QStringLiteral("PasswordProtectedHint")] = true; } qCDebug(ARK) << "Going to add files" << files << "with options" << newOptions; Q_ASSERT(!m_iface->isReadOnly()); - AddJob *newJob = new AddJob(files, destination, newOptions, static_cast(m_iface)); + AddJob *newJob = new AddJob(files, newOptions, static_cast(m_iface)); connect(newJob, &AddJob::result, this, &Archive::onAddFinished); return newJob; } ExtractJob* Archive::copyFiles(const QList &files, const QString& destinationDir, const ExtractionOptions& options) { if (!isValid()) { return Q_NULLPTR; } ExtractionOptions newOptions = options; if (encryptionType() != Unencrypted) { newOptions[QStringLiteral( "PasswordProtectedHint" )] = true; } ExtractJob *newJob = new ExtractJob(files, destinationDir, newOptions, m_iface); return newJob; } PreviewJob *Archive::preview(Archive::Entry *entry) { if (!isValid()) { return Q_NULLPTR; } PreviewJob *job = new PreviewJob(entry, (encryptionType() != Unencrypted), m_iface); return job; } OpenJob *Archive::open(Archive::Entry *entry) { if (!isValid()) { return Q_NULLPTR; } OpenJob *job = new OpenJob(entry, (encryptionType() != Unencrypted), m_iface); return job; } OpenWithJob *Archive::openWith(Archive::Entry *entry) { if (!isValid()) { return Q_NULLPTR; } OpenWithJob *job = new OpenWithJob(entry, (encryptionType() != Unencrypted), m_iface); return job; } void Archive::encrypt(const QString &password, bool encryptHeader) { if (!isValid()) { return; } m_iface->setPassword(password); m_iface->setHeaderEncryptionEnabled(encryptHeader); m_encryptionType = encryptHeader ? HeaderEncrypted : Encrypted; } void Archive::onAddFinished(KJob* job) { //if the archive was previously a single folder archive and an add job //has successfully finished, then it is no longer a single folder //archive (for the current implementation, which does not allow adding //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; } m_hasBeenListed = true; } 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 } } void Archive::onUserQuery(Query* query) { query->execute(); } } // namespace Kerfuffle diff --git a/kerfuffle/archive_kerfuffle.h b/kerfuffle/archive_kerfuffle.h index 79e5c33c..856b2b88 100644 --- a/kerfuffle/archive_kerfuffle.h +++ b/kerfuffle/archive_kerfuffle.h @@ -1,196 +1,196 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008 Harald Hvaal * Copyright (c) 2011 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef ARCHIVE_H #define ARCHIVE_H #include "kerfuffle_export.h" #include #include #include #include #include class KJob; namespace Kerfuffle { class ListJob; class ExtractJob; class DeleteJob; class AddJob; class CommentJob; class TestJob; class OpenJob; class OpenWithJob; class Plugin; class PreviewJob; class Query; class ReadOnlyArchiveInterface; enum ArchiveError { NoError = 0, NoPlugin, FailedPlugin }; /** These are the extra options for doing the compression. Naming convention is CamelCase with either Global, or the compression type (such as Zip, Rar, etc), followed by the property name used */ typedef QHash CompressionOptions; typedef QHash ExtractionOptions; class KERFUFFLE_EXPORT Archive : public QObject { Q_OBJECT Q_ENUMS(EncryptionType) /** * Complete base name, without the "tar" extension (if any). */ Q_PROPERTY(QString completeBaseName READ completeBaseName CONSTANT) Q_PROPERTY(QString fileName READ fileName CONSTANT) Q_PROPERTY(QString comment READ comment CONSTANT) Q_PROPERTY(QMimeType mimeType READ mimeType CONSTANT) Q_PROPERTY(bool isReadOnly READ isReadOnly CONSTANT) Q_PROPERTY(bool isSingleFolderArchive READ isSingleFolderArchive) Q_PROPERTY(EncryptionType encryptionType READ encryptionType) Q_PROPERTY(qulonglong numberOfFiles READ numberOfFiles) Q_PROPERTY(qulonglong unpackedSize READ unpackedSize) Q_PROPERTY(qulonglong packedSize READ packedSize) Q_PROPERTY(QString subfolderName READ subfolderName) public: enum EncryptionType { Unencrypted, Encrypted, HeaderEncrypted }; class Entry; QString completeBaseName() const; QString fileName() const; QString comment() const; QMimeType mimeType(); bool isReadOnly() const; bool isSingleFolderArchive(); bool hasComment() const; EncryptionType encryptionType(); qulonglong numberOfFiles(); qulonglong unpackedSize(); qulonglong packedSize() const; QString subfolderName(); 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); ~Archive(); ArchiveError error() const; bool isValid() const; KJob* open(); KJob* create(); /** * @return A ListJob if the archive already exists. A null pointer otherwise. */ ListJob* list(); DeleteJob* deleteFiles(QList &entries); CommentJob* addComment(const QString &comment); TestJob* testArchive(); /** * Compression options that should be handled by all interfaces: * * GlobalWorkDir - Change to this dir before adding the new files. * The path names should then be added relative to this directory. * * TODO: find a way to actually add files to specific locations in * the archive * (not supported yet) GlobalPathInArchive - a path relative to the * archive root where the files will be added under * */ - AddJob* addFiles(QList &files, const Archive::Entry *destination, const CompressionOptions& options = CompressionOptions()); + AddJob* addFiles(QList &files, const CompressionOptions& options = CompressionOptions()); ExtractJob* copyFiles(const QList &files, const QString &destinationDir, const ExtractionOptions &options = ExtractionOptions()); PreviewJob* preview(Archive::Entry *entry); OpenJob* open(Archive::Entry *entry); OpenWithJob* openWith(Archive::Entry *entry); /** * @param password The password to encrypt the archive with. * @param encryptHeader Whether to encrypt also the list of files. */ 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(); ReadOnlyArchiveInterface *m_iface; bool m_hasBeenListed; bool m_isReadOnly; bool m_isSingleFolderArchive; QString m_subfolderName; qulonglong m_extractedFilesSize; ArchiveError m_error; EncryptionType m_encryptionType; qulonglong m_numberOfFiles; QMimeType m_mimeType; }; } // namespace Kerfuffle Q_DECLARE_METATYPE(Kerfuffle::Archive::EncryptionType) #endif // ARCHIVE_H diff --git a/kerfuffle/jobs.cpp b/kerfuffle/jobs.cpp index 201f956f..5912cbed 100644 --- a/kerfuffle/jobs.cpp +++ b/kerfuffle/jobs.cpp @@ -1,563 +1,562 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 Raphael Kubo da Costa * * 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 //#define DEBUG_RACECONDITION namespace Kerfuffle { class Job::Private : public QThread { public: Private(Job *job, QObject *parent = 0) : QThread(parent) , q(job) { connect(q, &KJob::result, this, &QThread::quit); } virtual void run() Q_DECL_OVERRIDE; private: Job *q; }; void Job::Private::run() { q->doWork(); if (q->isRunning()) { exec(); } #ifdef DEBUG_RACECONDITION QThread::sleep(2); #endif } Job::Job(ReadOnlyArchiveInterface *interface) : KJob() , m_archiveInterface(interface) , m_isRunning(false) , d(new Private(this)) { static bool onlyOnce = false; if (!onlyOnce) { qRegisterMetaType >("QPair"); onlyOnce = true; } setCapabilities(KJob::Killable); } Job::~Job() { qDeleteAll(m_archiveEntries); m_archiveEntries.clear(); if (d->isRunning()) { d->wait(); } delete d; } ReadOnlyArchiveInterface *Job::archiveInterface() { return m_archiveInterface; } bool Job::isRunning() const { return m_isRunning; } void Job::start() { jobTimer.start(); m_isRunning = true; 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::emitResult() { m_isRunning = false; KJob::emitResult(); } 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::entryRemoved, this, &Job::onEntryRemoved); connect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &Job::onProgress); connect(archiveInterface(), &ReadOnlyArchiveInterface::info, this, &Job::onInfo); connect(archiveInterface(), &ReadOnlyArchiveInterface::finished, this, &Job::onFinished, Qt::DirectConnection); connect(archiveInterface(), &ReadOnlyArchiveInterface::userQuery, this, &Job::onUserQuery); } 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"; emitResult(); } void Job::onUserQuery(Query *query) { emit userQuery(query); } bool Job::doKill() { bool ret = archiveInterface()->doKill(); if (!ret) { qCWarning(ARK) << "Killing does not seem to be supported here."; } return ret; } ListJob::ListJob(ReadOnlyArchiveInterface *interface) : Job(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); } void ListJob::doWork() { emit description(this, i18n("Loading archive...")); connectToArchiveInterfaceSignals(); bool ret = archiveInterface()->list(); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } qlonglong ListJob::extractedFilesSize() const { return m_extractedFilesSize; } bool ListJob::isPasswordProtected() const { return m_isPasswordProtected; } bool ListJob::isSingleFolderArchive() const { if (m_filesCount == 1 && m_dirCount == 0) { return false; } return m_isSingleFolderArchive; } void ListJob::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->property("fullPath").toString().replace(QRegularExpression(QStringLiteral("^\\./")), QString()); const QString basePath = fullPath.split(QLatin1Char('/')).at(0); if (m_basePath.isEmpty()) { m_basePath = basePath; m_subfolderName = basePath; } else { if (m_basePath != basePath) { m_isSingleFolderArchive = false; m_subfolderName.clear(); } } } } QString ListJob::subfolderName() const { if (!isSingleFolderArchive()) { return QString(); } return m_subfolderName; } ExtractJob::ExtractJob(const QList &entries, const QString &destinationDir, const ExtractionOptions &options, ReadOnlyArchiveInterface *interface) : Job(interface) , m_entries(entries) , m_destinationDir(destinationDir) , m_options(options) { qCDebug(ARK) << "ExtractJob created"; setDefaultOptions(); } 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); 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 selected files:" << m_entries << "Destination dir:" << m_destinationDir << "Options:" << m_options; bool ret = archiveInterface()->copyFiles(m_entries, m_destinationDir, m_options); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void ExtractJob::setDefaultOptions() { ExtractionOptions defaultOptions; defaultOptions[QStringLiteral("PreservePaths")] = false; ExtractionOptions::const_iterator it = defaultOptions.constBegin(); for (; it != defaultOptions.constEnd(); ++it) { if (!m_options.contains(it.key())) { m_options[it.key()] = it.value(); } } } 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) { } QString TempExtractJob::validatedFilePath() const { QString path = extractionDir() + QLatin1Char('/') + m_entry->property("fullPath").toString(); // 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; options[QStringLiteral("PreservePaths")] = true; if (m_passwordProtectedHint) { options[QStringLiteral("PasswordProtectedHint")] = true; } return options; } void TempExtractJob::doWork() { emit description(this, i18n("Extracting one file")); connectToArchiveInterfaceSignals(); qCDebug(ARK) << "Extracting:" << m_entry; bool ret = archiveInterface()->copyFiles({ m_entry }, extractionDir(), extractionOptions()); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } PreviewJob::PreviewJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : TempExtractJob(entry, passwordProtectedHint, interface) { qCDebug(ARK) << "PreviewJob started"; } QString PreviewJob::extractionDir() const { return m_tmpExtractDir.path(); } OpenJob::OpenJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : TempExtractJob(entry, passwordProtectedHint, interface) { qCDebug(ARK) << "OpenJob started"; m_tmpExtractDir = new QTemporaryDir(); } QTemporaryDir *OpenJob::tempDir() const { return m_tmpExtractDir; } QString OpenJob::extractionDir() const { return m_tmpExtractDir->path(); } OpenWithJob::OpenWithJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : OpenJob(entry, passwordProtectedHint, interface) { qCDebug(ARK) << "OpenWithJob started"; } -AddJob::AddJob(QList &entries, const Archive::Entry *destination, const CompressionOptions& options , ReadWriteArchiveInterface *interface) +AddJob::AddJob(QList &entries, const CompressionOptions& options , ReadWriteArchiveInterface *interface) : Job(interface) , m_entries(entries) - , m_destination(destination) , m_options(options) { qCDebug(ARK) << "AddJob started"; } void AddJob::doWork() { qCDebug(ARK) << "AddJob: going to add" << m_entries.count() << "file(s)"; emit description(this, i18np("Adding a file", "Adding %1 files", m_entries.count())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); const QString globalWorkDir = m_options.value(QStringLiteral("GlobalWorkDir")).toString(); 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); } // The file paths must be relative to GlobalWorkDir. foreach (Archive::Entry *entry, m_entries) { // #191821: workDir must be used instead of QDir::current() // so that symlinks aren't resolved automatically const QString &fullPath = entry->property("fullPath").toString(); QString relativePath = workDir.relativeFilePath(fullPath); if (fullPath.endsWith(QLatin1Char('/'))) { relativePath += QLatin1Char('/'); } entry->setFullPath(relativePath); } connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->addFiles(m_entries, m_options); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void AddJob::onFinished(bool result) { if (!m_oldWorkingDir.isEmpty()) { QDir::setCurrent(m_oldWorkingDir); } Job::onFinished(result); } DeleteJob::DeleteJob(QList &entries, ReadWriteArchiveInterface *interface) : Job(interface) , m_entries(entries) { } void DeleteJob::doWork() { emit description(this, i18np("Deleting a file from the archive", "Deleting %1 files", m_entries.count())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->deleteFiles(m_entries); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } CommentJob::CommentJob(const QString& comment, ReadWriteArchiveInterface *interface) : Job(interface) , m_comment(comment) { } void CommentJob::doWork() { emit description(this, i18n("Adding comment")); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->addComment(m_comment); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } TestJob::TestJob(ReadOnlyArchiveInterface *interface) : Job(interface) { m_testSuccess = false; } void TestJob::doWork() { qCDebug(ARK) << "TestJob started"; emit description(this, i18n("Testing archive")); 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 diff --git a/kerfuffle/jobs.h b/kerfuffle/jobs.h index 2593d385..3279bffb 100644 --- a/kerfuffle/jobs.h +++ b/kerfuffle/jobs.h @@ -1,291 +1,290 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef JOBS_H #define JOBS_H #include "kerfuffle_export.h" #include "archiveinterface.h" #include "archive_kerfuffle.h" #include "archiveentry.h" #include "queries.h" #include #include #include namespace Kerfuffle { class KERFUFFLE_EXPORT Job : public KJob { Q_OBJECT public: void start(); bool isRunning() const; protected: Job(ReadOnlyArchiveInterface *interface); virtual ~Job(); virtual bool doKill(); virtual void emitResult(); ReadOnlyArchiveInterface *archiveInterface(); QList m_archiveEntries; void connectToArchiveInterfaceSignals(); public slots: virtual void doWork() = 0; protected slots: virtual void onCancelled(); virtual void onError(const QString &message, const QString &details); virtual void onInfo(const QString &info); virtual void onEntry(Archive::Entry *entry); virtual void onProgress(double progress); virtual void onEntryRemoved(const QString &path); virtual void onFinished(bool result); virtual void onUserQuery(Query *query); signals: void entryRemoved(const QString & entry); void error(const QString& errorMessage, const QString& details); void newEntry(Archive::Entry*); void userQuery(Kerfuffle::Query*); private: ReadOnlyArchiveInterface *m_archiveInterface; bool m_isRunning; QElapsedTimer jobTimer; class Private; Private * const d; }; class KERFUFFLE_EXPORT ListJob : public Job { Q_OBJECT public: explicit ListJob(ReadOnlyArchiveInterface *interface); qlonglong extractedFilesSize() const; bool isPasswordProtected() const; bool isSingleFolderArchive() const; QString subfolderName() const; public slots: virtual void doWork() Q_DECL_OVERRIDE; private: bool m_isSingleFolderArchive; bool m_isPasswordProtected; QString m_subfolderName; QString m_basePath; qlonglong m_extractedFilesSize; qlonglong m_dirCount; qlonglong m_filesCount; private slots: void onNewEntry(const Archive::Entry*); }; class KERFUFFLE_EXPORT ExtractJob : public Job { Q_OBJECT public: ExtractJob(const QList &entries, const QString& destinationDir, const ExtractionOptions& options, ReadOnlyArchiveInterface *interface); QString destinationDirectory() const; ExtractionOptions extractionOptions() const; public slots: virtual void doWork() Q_DECL_OVERRIDE; private: // TODO: Maybe this should be a method if ExtractionOptions were a class? void setDefaultOptions(); QList m_entries; QString m_destinationDir; ExtractionOptions m_options; }; /** * Abstract base class for jobs that extract a single file to a temporary dir. * It's not possible to pass extraction options and paths will be always preserved. * The only option that the job needs to know is whether the file is password protected. */ class KERFUFFLE_EXPORT TempExtractJob : public Job { Q_OBJECT public: TempExtractJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface); /** * @return The absolute path of the extracted file. * The path is validated in order to prevent directory traversal attacks. */ QString validatedFilePath() const; ExtractionOptions extractionOptions() const; public slots: virtual void doWork() Q_DECL_OVERRIDE; private: virtual QString extractionDir() const = 0; Archive::Entry *m_entry; bool m_passwordProtectedHint; }; /** * This TempExtractJob can be used to preview a file. * The temporary extraction directory will be deleted upon job's completion. */ class KERFUFFLE_EXPORT PreviewJob : public TempExtractJob { Q_OBJECT public: PreviewJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface); private: QString extractionDir() const Q_DECL_OVERRIDE; QTemporaryDir m_tmpExtractDir; }; /** * This TempExtractJob can be used to open a file in its dedicated application. * For this reason, the temporary extraction directory will NOT be deleted upon job's completion. */ class KERFUFFLE_EXPORT OpenJob : public TempExtractJob { Q_OBJECT public: OpenJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface); /** * @return The temporary dir used for the extraction. * It is safe to delete this pointer in order to remove the directory. */ QTemporaryDir *tempDir() const; private: QString extractionDir() const Q_DECL_OVERRIDE; QTemporaryDir *m_tmpExtractDir; }; class KERFUFFLE_EXPORT OpenWithJob : public OpenJob { Q_OBJECT public: OpenWithJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface); }; class KERFUFFLE_EXPORT AddJob : public Job { Q_OBJECT public: - AddJob(QList &files, const Archive::Entry *destination, const CompressionOptions& options, ReadWriteArchiveInterface *interface); + AddJob(QList &files, const CompressionOptions& options, ReadWriteArchiveInterface *interface); public slots: virtual void doWork() Q_DECL_OVERRIDE; protected slots: virtual void onFinished(bool result) Q_DECL_OVERRIDE; private: QString m_oldWorkingDir; QList m_entries; - const Archive::Entry *m_destination; CompressionOptions m_options; }; class KERFUFFLE_EXPORT DeleteJob : public Job { Q_OBJECT public: DeleteJob(QList &files, ReadWriteArchiveInterface *interface); public slots: virtual void doWork() Q_DECL_OVERRIDE; private: QList m_entries; }; class KERFUFFLE_EXPORT CommentJob : public Job { Q_OBJECT public: CommentJob(const QString& comment, ReadWriteArchiveInterface *interface); public slots: virtual void doWork() Q_DECL_OVERRIDE; private: QString m_comment; }; class KERFUFFLE_EXPORT TestJob : public Job { Q_OBJECT public: TestJob(ReadOnlyArchiveInterface *interface); bool testSucceeded(); public slots: virtual void doWork() Q_DECL_OVERRIDE; private slots: virtual void onTestSuccess(); private: bool m_testSuccess; }; } // namespace Kerfuffle #endif // JOBS_H diff --git a/part/archivemodel.cpp b/part/archivemodel.cpp index 75466ca5..ba880d62 100644 --- a/part/archivemodel.cpp +++ b/part/archivemodel.cpp @@ -1,886 +1,882 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 Harald Hvaal * Copyright (C) 2010-2012 Raphael Kubo da Costa * * 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 "archivemodel.h" #include "kerfuffle/jobs.h" #include #include #include #include #include #include using namespace Kerfuffle; //used to speed up the loading of large archives static Archive::Entry *s_previousMatch = Q_NULLPTR; Q_GLOBAL_STATIC(QStringList, s_previousPieces) /** * Meta data related to one entry in a compressed archive. * * This is used for indexing entry properties as numbers * and for determining data displaying order in part's view. */ enum EntryMetaDataType { FullPath, /**< The entry's file name */ Size, /**< The entry's original size */ CompressedSize, /**< The compressed size for the entry */ Permissions, /**< The entry's permissions */ Owner, /**< The user the entry belongs to */ Group, /**< The user group the entry belongs to */ Ratio, /**< The compression ratio for the entry */ CRC, /**< The entry's CRC */ Method, /**< The compression method used on the entry */ Version, /**< The archiver version needed to extract the entry */ Timestamp, /**< The timestamp for the current entry */ Comment, }; /** * Mappings between column indexes and entry properties. */ static QMap initializePropertiesList() { QMap propertiesList = QMap(); propertiesList.insert(FullPath, QStringLiteral("fullPath")); propertiesList.insert(Size, QStringLiteral("size")); propertiesList.insert(CompressedSize, QStringLiteral("compressedSize")); propertiesList.insert(Permissions, QStringLiteral("permissions")); propertiesList.insert(Owner, QStringLiteral("owner")); propertiesList.insert(Group, QStringLiteral("group")); propertiesList.insert(Ratio, QStringLiteral("ratio")); propertiesList.insert(CRC, QStringLiteral("CRC")); propertiesList.insert(Method, QStringLiteral("method")); propertiesList.insert(Version, QStringLiteral("version")); propertiesList.insert(Timestamp, QStringLiteral("timestamp")); propertiesList.insert(Comment, QStringLiteral("comment")); return propertiesList; } static const QMap propertiesList = initializePropertiesList(); /** * Helper functor used by qStableSort. * * It always sorts folders before files. * * @internal */ class ArchiveModelSorter { public: ArchiveModelSorter(int column, Qt::SortOrder order) : m_sortColumn(column) , m_sortOrder(order) { } virtual ~ArchiveModelSorter() { } inline bool operator()(const QPair &left, const QPair &right) const { if (m_sortOrder == Qt::AscendingOrder) { return lessThan(left, right); } else { return !lessThan(left, right); } } protected: bool lessThan(const QPair &left, const QPair &right) const { const Archive::Entry * const leftEntry = left.first; const Archive::Entry * const rightEntry = right.first; // #234373: sort folders before files if ((leftEntry->isDir()) && (!rightEntry->isDir())) { return (m_sortOrder == Qt::AscendingOrder); } else if ((!leftEntry->isDir()) && (rightEntry->isDir())) { return !(m_sortOrder == Qt::AscendingOrder); } EntryMetaDataType column = static_cast(m_sortColumn); const QVariant &leftEntryMetaData = leftEntry->property(propertiesList[column].toStdString().c_str()); const QVariant &rightEntryMetaData = rightEntry->property(propertiesList[column].toStdString().c_str()); switch (m_sortColumn) { case FullPath: return leftEntry->name() < rightEntry->name(); case Size: case CompressedSize: return leftEntryMetaData.toInt() < rightEntryMetaData.toInt(); default: return leftEntryMetaData.toString() < rightEntryMetaData.toString(); } // We should not get here. Q_ASSERT(false); return false; } private: int m_sortColumn; Qt::SortOrder m_sortOrder; }; ArchiveModel::ArchiveModel(const QString &dbusPathName, QObject *parent) : QAbstractItemModel(parent) , m_rootEntry() , m_dbusPathName(dbusPathName) { m_rootEntry.setProperty("isDirectory", true); } ArchiveModel::~ArchiveModel() { } QVariant ArchiveModel::data(const QModelIndex &index, int role) const { if (index.isValid()) { Archive::Entry *entry = static_cast(index.internalPointer()); switch (role) { case Qt::DisplayRole: { //TODO: complete the columns int column = m_showColumns.at(index.column()); switch (column) { case FullPath: return entry->name(); case Size: if (entry->isDir()) { int dirs; int files; const int children = childCount(index, dirs, files); return KIO::itemsSummaryString(children, files, dirs, 0, false); } else if (!entry->property("link").toString().isEmpty()) { return QVariant(); } else { return KIO::convertSize(entry->property("size").toULongLong()); } case CompressedSize: if (entry->isDir() || !entry->property("link").toString().isEmpty()) { return QVariant(); } else { qulonglong compressedSize = entry->property("compressedSize").toULongLong(); if (compressedSize != 0) { return KIO::convertSize(compressedSize); } else { return QVariant(); } } case Ratio: // TODO: Use entry->metaData()[Ratio] when available if (entry->isDir() || !entry->property("link").toString().isEmpty()) { return QVariant(); } else { qulonglong compressedSize = entry->property("compressedSize").toULongLong(); qulonglong size = entry->property("size").toULongLong(); if (compressedSize == 0 || size == 0) { return QVariant(); } else { int ratio = int(100 * ((double)size - compressedSize) / size); return QString(QString::number(ratio) + QStringLiteral(" %")); } } case Timestamp: { const QDateTime timeStamp = entry->property("timestamp").toDateTime(); return QLocale().toString(timeStamp, QLocale::ShortFormat); } default: return entry->property(propertiesList[column].toStdString().c_str()); } } case Qt::DecorationRole: if (index.column() == 0) { return entry->icon(); } return QVariant(); case Qt::FontRole: { QFont f; f.setItalic(entry->property("isPasswordProtected").toBool()); return f; } default: return QVariant(); } } return QVariant(); } Qt::ItemFlags ArchiveModel::flags(const QModelIndex &index) const { Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index); if (index.isValid()) { return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | defaultFlags; } return 0; } QVariant ArchiveModel::headerData(int section, Qt::Orientation, int role) const { if (role == Qt::DisplayRole) { if (section >= m_showColumns.size()) { qCDebug(ARK) << "WEIRD: showColumns.size = " << m_showColumns.size() << " and section = " << section; return QVariant(); } int columnId = m_showColumns.at(section); switch (columnId) { case FullPath: return i18nc("Name of a file inside an archive", "Name"); case Size: return i18nc("Uncompressed size of a file inside an archive", "Size"); case CompressedSize: return i18nc("Compressed size of a file inside an archive", "Compressed"); case Ratio: return i18nc("Compression rate of file", "Rate"); case Owner: return i18nc("File's owner username", "Owner"); case Group: return i18nc("File's group", "Group"); case Permissions: return i18nc("File permissions", "Mode"); case CRC: return i18nc("CRC hash code", "CRC"); case Method: return i18nc("Compression method", "Method"); case Version: //TODO: what exactly is a file version? return i18nc("File version", "Version"); case Timestamp: return i18nc("Timestamp", "Date"); case Comment: return i18nc("File comment", "Comment"); default: return i18nc("Unnamed column", "??"); } } return QVariant(); } QModelIndex ArchiveModel::index(int row, int column, const QModelIndex &parent) const { if (hasIndex(row, column, parent)) { const Archive::Entry *parentEntry = parent.isValid() ? static_cast(parent.internalPointer()) : &m_rootEntry; Q_ASSERT(parentEntry->isDir()); const Archive::Entry *item = parentEntry->entries().value(row, Q_NULLPTR); if (item != Q_NULLPTR) { return createIndex(row, column, const_cast(item)); } } return QModelIndex(); } QModelIndex ArchiveModel::parent(const QModelIndex &index) const { if (index.isValid()) { Archive::Entry *item = static_cast(index.internalPointer()); Q_ASSERT(item); if (item->getParent() && (item->getParent() != &m_rootEntry)) { return createIndex(item->getParent()->row(), 0, item->getParent()); } } return QModelIndex(); } Archive::Entry *ArchiveModel::entryForIndex(const QModelIndex &index) { if (index.isValid()) { Archive::Entry *item = static_cast(index.internalPointer()); Q_ASSERT(item); return item; } return Q_NULLPTR; } int ArchiveModel::childCount(const QModelIndex &index, int &dirs, int &files) const { if (index.isValid()) { dirs = files = 0; Archive::Entry *item = static_cast(index.internalPointer()); Q_ASSERT(item); if (item->isDir()) { const QVector entries = item->entries(); foreach(const Archive::Entry *entry, entries) { if (entry->isDir()) { dirs++; } else { files++; } } return entries.count(); } return 0; } return -1; } int ArchiveModel::rowCount(const QModelIndex &parent) const { if (parent.column() <= 0) { const Archive::Entry *parentEntry = parent.isValid() ? static_cast(parent.internalPointer()) : &m_rootEntry; if (parentEntry && parentEntry->isDir()) { return parentEntry->entries().count(); } } return 0; } int ArchiveModel::columnCount(const QModelIndex &parent) const { return m_showColumns.size(); } void ArchiveModel::sort(int column, Qt::SortOrder order) { if (m_showColumns.size() <= column) { return; } emit layoutAboutToBeChanged(); QList dirEntries; m_rootEntry.returnDirEntries(&dirEntries); dirEntries.append(&m_rootEntry); const ArchiveModelSorter modelSorter(m_showColumns.at(column), order); foreach(Archive::Entry *dir, dirEntries) { QVector < QPair > sorting(dir->entries().count()); for (int i = 0; i < dir->entries().count(); ++i) { Archive::Entry *item = dir->entries().at(i); sorting[i].first = item; sorting[i].second = i; } qStableSort(sorting.begin(), sorting.end(), modelSorter); QModelIndexList fromIndexes; QModelIndexList toIndexes; for (int r = 0; r < sorting.count(); ++r) { Archive::Entry *item = sorting.at(r).first; toIndexes.append(createIndex(r, 0, item)); fromIndexes.append(createIndex(sorting.at(r).second, 0, sorting.at(r).first)); dir->setEntryAt(r, sorting.at(r).first); } changePersistentIndexList(fromIndexes, toIndexes); emit dataChanged( index(0, 0, indexForEntry(dir)), index(dir->entries().size() - 1, 0, indexForEntry(dir))); } emit layoutChanged(); } Qt::DropActions ArchiveModel::supportedDropActions() const { return Qt::CopyAction | Qt::MoveAction; } QStringList ArchiveModel::mimeTypes() const { QStringList types; // MIME types we accept for dragging (eg. Dolphin -> Ark). types << QStringLiteral("text/uri-list") << QStringLiteral("text/plain") << QStringLiteral("text/x-moz-url"); // MIME types we accept for dropping (eg. Ark -> Dolphin). types << QStringLiteral("application/x-kde-ark-dndextract-service") << QStringLiteral("application/x-kde-ark-dndextract-path"); return types; } QMimeData *ArchiveModel::mimeData(const QModelIndexList &indexes) const { Q_UNUSED(indexes) QMimeData *mimeData = new QMimeData; mimeData->setData(QStringLiteral("application/x-kde-ark-dndextract-service"), QDBusConnection::sessionBus().baseService().toUtf8()); mimeData->setData(QStringLiteral("application/x-kde-ark-dndextract-path"), m_dbusPathName.toUtf8()); return mimeData; } bool ArchiveModel::dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) { Q_UNUSED(action) Q_UNUSED(row) Q_UNUSED(column) Q_UNUSED(parent) if (!data->hasUrls()) { return false; } QStringList paths; foreach(const QUrl &url, data->urls()) { paths << url.toLocalFile(); } //for now, this code is not used because adding files to paths inside the //archive is not supported yet. need a solution for this later. QString path; #if 0 if (parent.isValid()) { QModelIndex droppedOnto = index(row, column, parent); Archive::Entry *entry = entryForIndex(droppedOnto); if (entry->isDir()) { qCDebug(ARK) << "Using entry"; path = entry->fileName.toString(); } else { path = entryForIndex(parent)->fileName.toString(); } } qCDebug(ARK) << "Dropped onto " << path; #endif emit droppedFiles(paths, path); return true; } // For a rationale, see bugs #194241, #241967 and #355839 QString ArchiveModel::cleanFileName(const QString& fileName) { // Skip entries with filename "/" or "//" or "." // "." is present in ISO files QRegularExpression pattern(QStringLiteral("/+|\\.")); QRegularExpressionMatch match; if (fileName.contains(pattern, &match) && match.captured() == fileName) { qCDebug(ARK) << "Skipping entry with filename" << fileName; return QString(); } else if (fileName.startsWith(QLatin1String("./"))) { return fileName.mid(2); } return fileName; } Archive::Entry *ArchiveModel::parentFor(const Archive::Entry *entry) { QStringList pieces = entry->property("fullPath").toString().split(QLatin1Char( '/' ), QString::SkipEmptyParts); if (pieces.isEmpty()) { return Q_NULLPTR; } pieces.removeLast(); if (s_previousMatch) { //the number of path elements must be the same for the shortcut //to work if (s_previousPieces->count() == pieces.count()) { bool equal = true; //make sure all the pieces match up for (int i = 0; i < s_previousPieces->count(); ++i) { if (s_previousPieces->at(i) != pieces.at(i)) { equal = false; break; } } //if match return it if (equal) { return s_previousMatch; } } } Archive::Entry *parent = &m_rootEntry; foreach(const QString &piece, pieces) { Archive::Entry *entry = parent->find(piece); if (!entry) { // Directory entry will be traversed later (that happens for some archive formats, 7z for instance). // We have to create one before, in order to construct tree from its children, // and then delete the existing one (see ArchiveModel::newEntry). entry = new Archive::Entry(parent); entry->setProperty("fullPath", (parent == &m_rootEntry) ? piece : parent->property("fullPath").toString() + QLatin1Char( '/' ) + piece); entry->setProperty("isDirectory", true); insertEntry(entry); } if (!entry->isDir()) { Archive::Entry *e = new Archive::Entry(parent); copyEntryMetaData(e, entry); // Maybe we have both a file and a directory of the same name. // We avoid removing previous entries unless necessary. insertEntry(e); } parent = entry; } s_previousMatch = parent; *s_previousPieces = pieces; return parent; } QModelIndex ArchiveModel::indexForEntry(Archive::Entry *entry) { Q_ASSERT(entry); if (entry != &m_rootEntry) { Q_ASSERT(entry->getParent()); Q_ASSERT(entry->getParent()->isDir()); return createIndex(entry->row(), 0, entry); } return QModelIndex(); } void ArchiveModel::slotEntryRemoved(const QString & path) { const QString entryFileName(cleanFileName(path)); if (entryFileName.isEmpty()) { return; } Archive::Entry *entry = m_rootEntry.findByPath(entryFileName.split(QLatin1Char( '/' ), QString::SkipEmptyParts)); if (entry) { Archive::Entry *parent = entry->getParent(); QModelIndex index = indexForEntry(entry); Q_UNUSED(index); beginRemoveRows(indexForEntry(parent), entry->row(), entry->row()); //delete parent->entries()[ metaData->row() ]; //parent->entries()[ metaData->row() ] = 0; parent->removeEntryAt(entry->row()); endRemoveRows(); } } void ArchiveModel::slotUserQuery(Kerfuffle::Query *query) { query->execute(); } void ArchiveModel::slotNewEntryFromSetArchive(Archive::Entry *entry) { // we cache all entries that appear when opening a new archive // so we can all them together once it's done, this is a huge // performance improvement because we save from doing lots of // begin/endInsertRows m_newArchiveEntries.push_back(entry); } void ArchiveModel::slotNewEntry(Archive::Entry *entry) { newEntry(entry, NotifyViews); } void ArchiveModel::newEntry(Archive::Entry *receivedEntry, InsertBehaviour behaviour) { if (receivedEntry->property("fullPath").toString().isEmpty()) { qCDebug(ARK) << "Weird, received empty entry (no filename) - skipping"; return; } //if there are no addidional columns registered, then have a look at the //entry and populate some if (m_showColumns.isEmpty()) { QList toInsert; QMap::const_iterator i = propertiesList.begin(); while (i != propertiesList.end()) { if (!receivedEntry->property(i.value().toStdString().c_str()).toString().isEmpty()) { if (i.key() != CompressedSize || receivedEntry->compressedSizeIsSet) { toInsert << i.key(); } } ++i; } beginInsertColumns(QModelIndex(), 0, toInsert.size() - 1); m_showColumns << toInsert; endInsertColumns(); qCDebug(ARK) << "Showing columns: " << m_showColumns; } //#194241: Filenames such as "./file" should be displayed as "file" //#241967: Entries called "/" should be ignored //#355839: Entries called "//" should be ignored QString entryFileName = cleanFileName(receivedEntry->property("fullPath").toString()); if (entryFileName.isEmpty()) { // The entry contains only "." or "./" return; } receivedEntry->setProperty("fullPath", entryFileName); /// 1. Skip already created entries Archive::Entry *existing = m_rootEntry.findByPath(entryFileName.split(QLatin1Char( '/' ))); if (existing) { qCDebug(ARK) << "Refreshing entry for" << entryFileName; existing->setProperty("fullPath", entryFileName); // Multi-volume files are repeated at least in RAR archives. // In that case, we need to sum the compressed size for each volume qulonglong currentCompressedSize = existing->property("compressedSize").toULongLong(); existing->setProperty("compressedSize", currentCompressedSize + receivedEntry->property("compressedSize").toULongLong()); return; } /// 2. Find Parent Entry, creating missing direcotry ArchiveEntries in the process Archive::Entry *parent = parentFor(receivedEntry); /// 3. Create an Archive::Entry const QStringList path = entryFileName.split(QLatin1Char('/'), QString::SkipEmptyParts); const QString name = path.last(); Archive::Entry *entry = parent->find(name); if (entry) { copyEntryMetaData(entry, receivedEntry); entry->setProperty("fullPath", entryFileName); delete receivedEntry; } else { receivedEntry->setParent(parent); insertEntry(receivedEntry, behaviour); } } void ArchiveModel::slotLoadingFinished(KJob *job) { int i = 0; foreach(Archive::Entry *entry, m_newArchiveEntries) { newEntry(entry, DoNotNotifyViews); i++; } beginResetModel(); endResetModel(); m_newArchiveEntries.clear(); qCDebug(ARK) << "Added" << i << "entries to model"; emit loadingFinished(job); } void ArchiveModel::copyEntryMetaData(Archive::Entry *destinationEntry, const Archive::Entry *sourceEntry) { destinationEntry->setProperty("fullPath", sourceEntry->property("fullPath")); destinationEntry->setProperty("permissions", sourceEntry->property("permissions")); destinationEntry->setProperty("owner", sourceEntry->property("owner")); destinationEntry->setProperty("group", sourceEntry->property("group")); destinationEntry->setProperty("size", sourceEntry->property("size")); destinationEntry->setProperty("compressedSize", sourceEntry->property("compressedSize")); destinationEntry->setProperty("link", sourceEntry->property("link")); destinationEntry->setProperty("ratio", sourceEntry->property("ratio")); destinationEntry->setProperty("CRC", sourceEntry->property("CRC")); destinationEntry->setProperty("method", sourceEntry->property("method")); destinationEntry->setProperty("version", sourceEntry->property("version")); destinationEntry->setProperty("timestamp", sourceEntry->property("timestamp").toDateTime()); destinationEntry->setProperty("isDirectory", sourceEntry->property("isDirectory")); destinationEntry->setProperty("comment", sourceEntry->property("comment")); destinationEntry->setProperty("isPasswordProtected", sourceEntry->property("isPasswordProtected")); } void ArchiveModel::insertEntry(Archive::Entry *entry, InsertBehaviour behaviour) { Q_ASSERT(entry); Archive::Entry *parent = entry->getParent(); Q_ASSERT(parent); if (behaviour == NotifyViews) { beginInsertRows(indexForEntry(parent), parent->entries().count(), parent->entries().count()); } parent->appendEntry(entry); if (behaviour == NotifyViews) { endInsertRows(); } } Kerfuffle::Archive* ArchiveModel::archive() const { return m_archive.data(); } KJob* ArchiveModel::setArchive(Kerfuffle::Archive *archive) { m_archive.reset(archive); 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(); } } beginResetModel(); endResetModel(); return job; } ExtractJob* ArchiveModel::extractFile(Archive::Entry *file, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options) const { QList files; files << file; return extractFiles(files, destinationDir, options); } ExtractJob* ArchiveModel::extractFiles(const QList& files, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options) const { Q_ASSERT(m_archive); ExtractJob *newJob = m_archive->copyFiles(files, destinationDir, options); connect(newJob, &ExtractJob::userQuery, this, &ArchiveModel::slotUserQuery); return newJob; } Kerfuffle::PreviewJob *ArchiveModel::preview(Archive::Entry *file) const { Q_ASSERT(m_archive); PreviewJob *job = m_archive->preview(file); connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery); return job; } OpenJob *ArchiveModel::open(Archive::Entry *file) const { Q_ASSERT(m_archive); OpenJob *job = m_archive->open(file); connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery); return job; } OpenWithJob *ArchiveModel::openWith(Archive::Entry *file) const { Q_ASSERT(m_archive); OpenWithJob *job = m_archive->openWith(file); connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery); return job; } -AddJob* ArchiveModel::addFiles(QList &entries, const Archive::Entry *destination, const CompressionOptions& options) +AddJob* ArchiveModel::addFiles(QList &entries, const CompressionOptions& options) { if (!m_archive) { return Q_NULLPTR; } - if (destination == Q_NULLPTR) { - destination = &m_rootEntry; - } - if (!m_archive->isReadOnly()) { - AddJob *job = m_archive->addFiles(entries, destination, options); + AddJob *job = m_archive->addFiles(entries, options); connect(job, &AddJob::newEntry, this, &ArchiveModel::slotNewEntry); connect(job, &AddJob::userQuery, this, &ArchiveModel::slotUserQuery); return job; } return Q_NULLPTR; } DeleteJob* ArchiveModel::deleteFiles(QList entries) { Q_ASSERT(m_archive); if (!m_archive->isReadOnly()) { DeleteJob *job = m_archive->deleteFiles(entries); connect(job, &DeleteJob::entryRemoved, this, &ArchiveModel::slotEntryRemoved); connect(job, &DeleteJob::finished, this, &ArchiveModel::slotCleanupEmptyDirs); connect(job, &DeleteJob::userQuery, this, &ArchiveModel::slotUserQuery); return job; } return Q_NULLPTR; } void ArchiveModel::encryptArchive(const QString &password, bool encryptHeader) { if (!m_archive) { return; } m_archive->encrypt(password, encryptHeader); } void ArchiveModel::slotCleanupEmptyDirs() { QList queue; QList nodesToDelete; //add root nodes for (int i = 0; i < rowCount(); ++i) { queue.append(QPersistentModelIndex(index(i, 0))); } //breadth-first traverse while (!queue.isEmpty()) { QPersistentModelIndex node = queue.takeFirst(); Archive::Entry *entry = entryForIndex(node); if (!hasChildren(node)) { if (entry->property("fullPath").toString().isEmpty()) { nodesToDelete << node; } } else { for (int i = 0; i < rowCount(node); ++i) { queue.append(QPersistentModelIndex(index(i, 0, node))); } } } foreach(const QPersistentModelIndex& node, nodesToDelete) { Archive::Entry *rawEntry = static_cast(node.internalPointer()); qCDebug(ARK) << "Delete with parent entries " << rawEntry->getParent()->entries() << " and row " << rawEntry->row(); beginRemoveRows(parent(node), rawEntry->row(), rawEntry->row()); rawEntry->getParent()->removeEntryAt(rawEntry->row()); endRemoveRows(); } } diff --git a/part/archivemodel.h b/part/archivemodel.h index eec780ea..b8935374 100644 --- a/part/archivemodel.h +++ b/part/archivemodel.h @@ -1,133 +1,133 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 Harald Hvaal * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #ifndef ARCHIVEMODEL_H #define ARCHIVEMODEL_H #include #include #include #include "kerfuffle/archiveentry.h" using Kerfuffle::Archive; namespace Kerfuffle { class Query; } class ArchiveModel: public QAbstractItemModel { Q_OBJECT public: explicit ArchiveModel(const QString &dbusPathName, QObject *parent = 0); ~ArchiveModel(); QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE; Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; QModelIndex parent(const QModelIndex &index) const Q_DECL_OVERRIDE; int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; virtual void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) Q_DECL_OVERRIDE; //drag and drop related Qt::DropActions supportedDropActions() const Q_DECL_OVERRIDE; QStringList mimeTypes() const Q_DECL_OVERRIDE; 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); Kerfuffle::Archive *archive() const; Archive::Entry *entryForIndex(const QModelIndex &index); int childCount(const QModelIndex &index, int &dirs, int &files) const; Kerfuffle::ExtractJob* extractFile(Archive::Entry *file, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options = Kerfuffle::ExtractionOptions()) const; Kerfuffle::ExtractJob* extractFiles(const QList& files, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options = Kerfuffle::ExtractionOptions()) const; Kerfuffle::PreviewJob* preview(Archive::Entry *file) const; Kerfuffle::OpenJob* open(Archive::Entry *file) const; Kerfuffle::OpenWithJob* openWith(Archive::Entry *file) const; - Kerfuffle::AddJob* addFiles(QList &entries, const Archive::Entry *destination, const Kerfuffle::CompressionOptions& options = Kerfuffle::CompressionOptions()); + Kerfuffle::AddJob* addFiles(QList &entries, const Kerfuffle::CompressionOptions& options = Kerfuffle::CompressionOptions()); Kerfuffle::DeleteJob* deleteFiles(QList entries); /** * @param password The password to encrypt the archive with. * @param encryptHeader Whether to encrypt also the list of files. */ void encryptArchive(const QString &password, bool encryptHeader); signals: void loadingStarted(); void loadingFinished(KJob *); void extractionFinished(bool success); void error(const QString& error, const QString& details); void droppedFiles(const QStringList& files, const QString& path = QString()); private slots: void slotNewEntryFromSetArchive(Archive::Entry *entry); void slotNewEntry(Archive::Entry *entry); void slotLoadingFinished(KJob *job); void slotEntryRemoved(const QString & path); void slotUserQuery(Kerfuffle::Query *query); void slotCleanupEmptyDirs(); private: /** * Strips file names that start with './'. * * For more information, see bug 194241. * * @param fileName The file name that will be stripped. * * @return @p fileName without the leading './' */ QString cleanFileName(const QString& fileName); Archive::Entry *parentFor(const Kerfuffle::Archive::Entry *entry); QModelIndex indexForEntry(Archive::Entry *entry); static bool compareAscending(const QModelIndex& a, const QModelIndex& b); static bool compareDescending(const QModelIndex& a, const QModelIndex& b); /** * Insert the node @p node into the model, ensuring all views are notified * of the change. */ enum InsertBehaviour { NotifyViews, DoNotNotifyViews }; void copyEntryMetaData(Archive::Entry *destinationEntry, const Archive::Entry *sourceEntry); void insertEntry(Archive::Entry *entry, InsertBehaviour behaviour = NotifyViews); void newEntry(Kerfuffle::Archive::Entry *receivedEntry, InsertBehaviour behaviour); QList m_newArchiveEntries; // holds entries from opening a new archive until it's totally open QList m_showColumns; QScopedPointer m_archive; Archive::Entry m_rootEntry; QString m_dbusPathName; }; #endif // ARCHIVEMODEL_H diff --git a/part/part.cpp b/part/part.cpp index 0f718f6f..bf1d0bc7 100644 --- a/part/part.cpp +++ b/part/part.cpp @@ -1,1372 +1,1372 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 Harald Hvaal * Copyright (C) 2009-2012 Raphael Kubo da Costa * * 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 "part.h" #include "ark_debug.h" #include "archiveformat.h" #include "archivemodel.h" #include "archiveview.h" #include "arkviewer.h" #include "dnddbusinterfaceadaptor.h" #include "infopanel.h" #include "jobtracker.h" #include "kerfuffle/extractiondialog.h" #include "kerfuffle/extractionsettingspage.h" #include "kerfuffle/jobs.h" #include "kerfuffle/settings.h" #include "kerfuffle/previewsettingspage.h" #include "kerfuffle/propertiesdialog.h" #include "pluginmanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kerfuffle; K_PLUGIN_FACTORY(Factory, registerPlugin();) namespace Ark { static quint32 s_instanceCounter = 1; Part::Part(QWidget *parentWidget, QObject *parent, const QVariantList& args) : KParts::ReadWritePart(parent), m_splitter(Q_NULLPTR), m_busy(false), m_jobTracker(Q_NULLPTR) { Q_UNUSED(args) setComponentData(*createAboutData(), false); new DndExtractAdaptor(this); const QString pathName = QStringLiteral("/DndExtract/%1").arg(s_instanceCounter++); if (!QDBusConnection::sessionBus().registerObject(pathName, this)) { qCCritical(ARK) << "Could not register a D-Bus object for drag'n'drop"; } // m_vlayout is needed for later insertion of QMessageWidget QWidget *mainWidget = new QWidget; m_vlayout = new QVBoxLayout; m_model = new ArchiveModel(pathName, this); m_splitter = new QSplitter(Qt::Horizontal, parentWidget); m_view = new ArchiveView; m_infoPanel = new InfoPanel(m_model); // Add widgets for the comment field. m_commentView = new QPlainTextEdit(); m_commentView->setReadOnly(true); m_commentView->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); m_commentBox = new QGroupBox(i18n("Comment")); m_commentBox->hide(); QVBoxLayout *vbox = new QVBoxLayout; vbox->addWidget(m_commentView); m_commentBox->setLayout(vbox); m_commentMsgWidget = new KMessageWidget(); m_commentMsgWidget->setText(i18n("Comment has been modified.")); m_commentMsgWidget->setMessageType(KMessageWidget::Information); m_commentMsgWidget->setCloseButtonVisible(false); m_commentMsgWidget->hide(); QAction *saveAction = new QAction(i18n("Save"), m_commentMsgWidget); m_commentMsgWidget->addAction(saveAction); connect(saveAction, &QAction::triggered, this, &Part::slotAddComment); m_commentBox->layout()->addWidget(m_commentMsgWidget); connect(m_commentView, &QPlainTextEdit::textChanged, this, &Part::slotCommentChanged); setWidget(mainWidget); mainWidget->setLayout(m_vlayout); // Configure the QVBoxLayout and add widgets m_vlayout->setContentsMargins(0,0,0,0); m_vlayout->addWidget(m_splitter); // Vertical QSplitter for the file view and comment field. m_commentSplitter = new QSplitter(Qt::Vertical, parentWidget); m_commentSplitter->setOpaqueResize(false); m_commentSplitter->addWidget(m_view); m_commentSplitter->addWidget(m_commentBox); m_commentSplitter->setCollapsible(0, false); // Horizontal QSplitter for the file view and infopanel. m_splitter->addWidget(m_commentSplitter); m_splitter->addWidget(m_infoPanel); // Read settings from config file and show/hide infoPanel. if (!ArkSettings::showInfoPanel()) { m_infoPanel->hide(); } else { m_splitter->setSizes(ArkSettings::splitterSizes()); } setupView(); setupActions(); connect(m_model, &ArchiveModel::loadingStarted, this, &Part::slotLoadingStarted); connect(m_model, &ArchiveModel::loadingFinished, this, &Part::slotLoadingFinished); connect(m_model, &ArchiveModel::droppedFiles, this, static_cast(&Part::slotAddFiles)); connect(m_model, &ArchiveModel::error, this, &Part::slotError); connect(this, &Part::busy, this, &Part::setBusyGui); connect(this, &Part::ready, this, &Part::setReadyGui); connect(this, static_cast(&KParts::ReadOnlyPart::completed), this, &Part::setFileNameFromArchive); m_statusBarExtension = new KParts::StatusBarExtension(this); setXMLFile(QStringLiteral("ark_part.rc")); } Part::~Part() { qDeleteAll(m_tmpOpenDirList); // Only save splitterSizes if infopanel is visible, // because we don't want to store zero size for infopanel. if (m_showInfoPanelAction->isChecked()) { ArkSettings::setSplitterSizes(m_splitter->sizes()); } ArkSettings::setShowInfoPanel(m_showInfoPanelAction->isChecked()); ArkSettings::self()->save(); m_extractArchiveAction->menu()->deleteLater(); m_extractAction->menu()->deleteLater(); } void Part::slotCommentChanged() { if (m_commentMsgWidget->isHidden() && m_commentView->toPlainText() != m_model->archive()->comment()) { m_commentMsgWidget->animatedShow(); } else if (m_commentMsgWidget->isVisible() && m_commentView->toPlainText() == m_model->archive()->comment()) { m_commentMsgWidget->hide(); } } KAboutData *Part::createAboutData() { return new KAboutData(QStringLiteral("ark"), i18n("ArkPart"), QStringLiteral("3.0")); } void Part::registerJob(KJob* job) { if (!m_jobTracker) { m_jobTracker = new JobTracker(widget()); m_statusBarExtension->addStatusBarItem(m_jobTracker->widget(0), 0, true); m_jobTracker->widget(job)->show(); } m_jobTracker->registerJob(job); emit busy(); connect(job, &KJob::result, this, &Part::ready); } // TODO: KIO::mostLocalHere is used here to resolve some KIO URLs to local // paths (e.g. desktop:/), but more work is needed to support extraction // to non-local destinations. See bugs #189322 and #204323. void Part::extractSelectedFilesTo(const QString& localPath) { if (!m_model) { return; } const QUrl url = QUrl::fromUserInput(localPath, QString()); KIO::StatJob* statJob = nullptr; // Try to resolve the URL to a local path. if (!url.isLocalFile() && !url.scheme().isEmpty()) { statJob = KIO::mostLocalUrl(url); if (!statJob->exec() || statJob->error() != 0) { return; } } const QString destination = statJob ? statJob->statResult().stringValue(KIO::UDSEntry::UDS_LOCAL_PATH) : localPath; delete statJob; // The URL could not be resolved to a local path. if (!url.isLocalFile() && destination.isEmpty()) { qCWarning(ARK) << "Ark cannot extract to non-local destination:" << localPath; KMessageBox::sorry(widget(), xi18nc("@info", "Ark can only extract to local destinations.")); return; } qCDebug(ARK) << "Extract to" << destination; Kerfuffle::ExtractionOptions options; options[QStringLiteral("PreservePaths")] = true; options[QStringLiteral("RemoveRootNode")] = true; options[QStringLiteral("DragAndDrop")] = true; // Create and start the ExtractJob. ExtractJob *job = m_model->extractFiles(filesAndRootNodesForIndexes(addChildren(m_view->selectionModel()->selectedRows())), destination, options); registerJob(job); connect(job, &KJob::result, this, &Part::slotExtractionDone); job->start(); } void Part::setupView() { m_view->setContextMenuPolicy(Qt::CustomContextMenu); m_view->setModel(m_model); m_view->setSortingEnabled(true); connect(m_view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Part::updateActions); connect(m_view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Part::selectionChanged); connect(m_view, &QTreeView::activated, this, &Part::slotActivated); connect(m_view, &QWidget::customContextMenuRequested, this, &Part::slotShowContextMenu); connect(m_model, &QAbstractItemModel::columnsInserted, this, &Part::adjustColumns); } void Part::slotActivated(QModelIndex) { // The activated signal is emitted when items are selected with the mouse, // so do nothing if CTRL or SHIFT key is pressed. if (QGuiApplication::keyboardModifiers() != Qt::ShiftModifier && QGuiApplication::keyboardModifiers() != Qt::ControlModifier) { ArkSettings::defaultOpenAction() == ArkSettings::EnumDefaultOpenAction::Preview ? slotOpenEntry(Preview) : slotOpenEntry(OpenFile); } } void Part::setupActions() { // We use a QSignalMapper for the preview, open and openwith actions. This // way we can connect all three actions to the same slot slotOpenEntry and // pass the OpenFileMode as argument to the slot. m_signalMapper = new QSignalMapper; m_showInfoPanelAction = new KToggleAction(i18nc("@action:inmenu", "Show information panel"), this); actionCollection()->addAction(QStringLiteral( "show-infopanel" ), m_showInfoPanelAction); m_showInfoPanelAction->setChecked(ArkSettings::showInfoPanel()); connect(m_showInfoPanelAction, &QAction::triggered, this, &Part::slotToggleInfoPanel); m_saveAsAction = actionCollection()->addAction(KStandardAction::SaveAs, QStringLiteral("ark_file_save_as"), this, SLOT(slotSaveAs())); m_openFileAction = actionCollection()->addAction(QStringLiteral("openfile")); m_openFileAction->setText(i18nc("open a file with external program", "&Open")); m_openFileAction->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); m_openFileAction->setToolTip(i18nc("@info:tooltip", "Click to open the selected file with the associated application")); connect(m_openFileAction, SIGNAL(triggered(bool)), m_signalMapper, SLOT(map())); m_signalMapper->setMapping(m_openFileAction, OpenFile); m_openFileWithAction = actionCollection()->addAction(QStringLiteral("openfilewith")); m_openFileWithAction->setText(i18nc("open a file with external program", "Open &With...")); m_openFileWithAction->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); m_openFileWithAction->setToolTip(i18nc("@info:tooltip", "Click to open the selected file with an external program")); connect(m_openFileWithAction, SIGNAL(triggered(bool)), m_signalMapper, SLOT(map())); m_signalMapper->setMapping(m_openFileWithAction, OpenFileWith); m_previewAction = actionCollection()->addAction(QStringLiteral("preview")); m_previewAction->setText(i18nc("to preview a file inside an archive", "Pre&view")); m_previewAction->setIcon(QIcon::fromTheme(QStringLiteral("document-preview-archive"))); m_previewAction->setToolTip(i18nc("@info:tooltip", "Click to preview the selected file")); actionCollection()->setDefaultShortcut(m_previewAction, Qt::CTRL + Qt::Key_P); connect(m_previewAction, SIGNAL(triggered(bool)), m_signalMapper, SLOT(map())); m_signalMapper->setMapping(m_previewAction, Preview); m_extractArchiveAction = actionCollection()->addAction(QStringLiteral("extract_all")); m_extractArchiveAction->setText(i18nc("@action:inmenu", "E&xtract All")); m_extractArchiveAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract"))); m_extractArchiveAction->setToolTip(i18n("Click to open an extraction dialog, where you can choose how to extract all the files in the archive")); actionCollection()->setDefaultShortcut(m_extractArchiveAction, Qt::CTRL + Qt::SHIFT + Qt::Key_E); connect(m_extractArchiveAction, &QAction::triggered, this, &Part::slotExtractArchive); m_extractAction = actionCollection()->addAction(QStringLiteral("extract")); m_extractAction->setText(i18nc("@action:inmenu", "&Extract")); m_extractAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract"))); actionCollection()->setDefaultShortcut(m_extractAction, Qt::CTRL + Qt::Key_E); m_extractAction->setToolTip(i18n("Click to open an extraction dialog, where you can choose to extract either all files or just the selected ones")); connect(m_extractAction, &QAction::triggered, this, &Part::slotShowExtractionDialog); m_addFilesAction = actionCollection()->addAction(QStringLiteral("add")); m_addFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-insert"))); m_addFilesAction->setText(i18n("Add &File...")); m_addFilesAction->setToolTip(i18nc("@info:tooltip", "Click to add files to the archive")); connect(m_addFilesAction, SIGNAL(triggered(bool)), this, SLOT(slotAddFiles())); m_addDirAction = actionCollection()->addAction(QStringLiteral("add-dir")); m_addDirAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-insert-directory"))); m_addDirAction->setText(i18n("Add Fo&lder...")); m_addDirAction->setToolTip(i18nc("@info:tooltip", "Click to add a folder to the archive")); connect(m_addDirAction, &QAction::triggered, this, &Part::slotAddDir); m_deleteFilesAction = actionCollection()->addAction(QStringLiteral("delete")); m_deleteFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-remove"))); m_deleteFilesAction->setText(i18n("De&lete")); actionCollection()->setDefaultShortcut(m_deleteFilesAction, Qt::Key_Delete); m_deleteFilesAction->setToolTip(i18nc("@info:tooltip", "Click to delete the selected files")); connect(m_deleteFilesAction, &QAction::triggered, this, &Part::slotDeleteFiles); m_propertiesAction = actionCollection()->addAction(QStringLiteral("properties")); m_propertiesAction->setIcon(QIcon::fromTheme(QStringLiteral("document-properties"))); m_propertiesAction->setText(i18nc("@action:inmenu", "&Properties")); actionCollection()->setDefaultShortcut(m_propertiesAction, Qt::ALT + Qt::Key_Return); m_propertiesAction->setToolTip(i18nc("@info:tooltip", "Click to see properties for archive")); connect(m_propertiesAction, &QAction::triggered, this, &Part::slotShowProperties); m_editCommentAction = actionCollection()->addAction(QStringLiteral("edit_comment")); m_editCommentAction->setIcon(QIcon::fromTheme(QStringLiteral("document-edit"))); actionCollection()->setDefaultShortcut(m_editCommentAction, Qt::ALT + Qt::Key_C); m_editCommentAction->setToolTip(i18nc("@info:tooltip", "Click to add or edit comment")); connect(m_editCommentAction, &QAction::triggered, this, &Part::slotShowComment); m_testArchiveAction = actionCollection()->addAction(QStringLiteral("test_archive")); m_testArchiveAction->setIcon(QIcon::fromTheme(QStringLiteral("checkmark"))); m_testArchiveAction->setText(i18nc("@action:inmenu", "&Test Integrity")); actionCollection()->setDefaultShortcut(m_testArchiveAction, Qt::ALT + Qt::Key_T); m_testArchiveAction->setToolTip(i18nc("@info:tooltip", "Click to test the archive for integrity")); connect(m_testArchiveAction, &QAction::triggered, this, &Part::slotTestArchive); connect(m_signalMapper, static_cast(&QSignalMapper::mapped), this, &Part::slotOpenEntry); updateActions(); updateQuickExtractMenu(m_extractArchiveAction); updateQuickExtractMenu(m_extractAction); } void Part::updateActions() { bool isWritable = m_model->archive() && !m_model->archive()->isReadOnly(); const Archive::Entry *entry = m_model->entryForIndex(m_view->selectionModel()->currentIndex()); int selectedEntriesCount = m_view->selectionModel()->selectedRows().count(); // Figure out if entry size is larger than preview size limit. const int maxPreviewSize = ArkSettings::previewFileSizeLimit() * 1024 * 1024; const bool limit = ArkSettings::limitPreviewFileSize(); bool isPreviewable = (!limit || (limit && entry != Q_NULLPTR && entry->property("size").toLongLong() < maxPreviewSize)); m_previewAction->setEnabled(!isBusy() && isPreviewable && !entry->isDir() && (selectedEntriesCount == 1)); m_extractArchiveAction->setEnabled(!isBusy() && (m_model->rowCount() > 0)); m_extractAction->setEnabled(!isBusy() && (m_model->rowCount() > 0)); m_saveAsAction->setEnabled(!isBusy() && m_model->rowCount() > 0); m_addFilesAction->setEnabled(!isBusy() && isWritable); m_addDirAction->setEnabled(!isBusy() && isWritable); m_deleteFilesAction->setEnabled(!isBusy() && isWritable && (selectedEntriesCount > 0)); m_openFileAction->setEnabled(!isBusy() && isPreviewable && !entry->isDir() && (selectedEntriesCount == 1)); m_openFileWithAction->setEnabled(!isBusy() && isPreviewable && !entry->isDir() && (selectedEntriesCount == 1)); m_propertiesAction->setEnabled(!isBusy() && m_model->archive()); m_commentView->setEnabled(!isBusy()); m_commentMsgWidget->setEnabled(!isBusy()); m_editCommentAction->setEnabled(false); m_testArchiveAction->setEnabled(false); if (m_model->archive()) { const KPluginMetaData metadata = PluginManager().preferredPluginFor(m_model->archive()->mimeType())->metaData(); bool supportsWriteComment = ArchiveFormat::fromMetadata(m_model->archive()->mimeType(), metadata).supportsWriteComment(); m_editCommentAction->setEnabled(!isBusy() && supportsWriteComment); m_commentView->setReadOnly(!supportsWriteComment); m_editCommentAction->setText(m_model->archive()->hasComment() ? i18nc("@action:inmenu mutually exclusive with Add &Comment", "Edit &Comment") : i18nc("@action:inmenu mutually exclusive with Edit &Comment", "Add &Comment")); bool supportsTesting = ArchiveFormat::fromMetadata(m_model->archive()->mimeType(), metadata).supportsTesting(); m_testArchiveAction->setEnabled(!isBusy() && supportsTesting); } else { m_commentView->setReadOnly(true); m_editCommentAction->setText(i18nc("@action:inmenu mutually exclusive with Edit &Comment", "Add &Comment")); } } void Part::slotShowComment() { if (!m_commentBox->isVisible()) { m_commentBox->show(); m_commentSplitter->setSizes(QList() << m_view->height() * 0.6 << 1); } m_commentView->setFocus(); } void Part::slotAddComment() { CommentJob *job = m_model->archive()->addComment(m_commentView->toPlainText()); if (!job) { return; } registerJob(job); job->start(); m_commentMsgWidget->hide(); if (m_commentView->toPlainText().isEmpty()) { m_commentBox->hide(); } } void Part::slotTestArchive() { TestJob *job = m_model->archive()->testArchive(); if (!job) { return; } registerJob(job); connect(job, &KJob::result, this, &Part::slotTestingDone); job->start(); } void Part::slotTestingDone(KJob* job) { if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } else if (static_cast(job)->testSucceeded()) { KMessageBox::information(widget(), i18n("The archive passed the integrity test."), i18n("Test Results")); } else { KMessageBox::error(widget(), i18n("The archive failed the integrity test."), i18n("Test Results")); } } void Part::updateQuickExtractMenu(QAction *extractAction) { if (!extractAction) { return; } QMenu *menu = extractAction->menu(); if (!menu) { menu = new QMenu(); extractAction->setMenu(menu); connect(menu, &QMenu::triggered, this, &Part::slotQuickExtractFiles); // Remember to keep this action's properties as similar to // extractAction's as possible (except where it does not make // sense, such as the text or the shortcut). QAction *extractTo = menu->addAction(i18n("Extract To...")); extractTo->setIcon(extractAction->icon()); extractTo->setToolTip(extractAction->toolTip()); if (extractAction == m_extractArchiveAction) { connect(extractTo, &QAction::triggered, this, &Part::slotExtractArchive); } else { connect(extractTo, &QAction::triggered, this, &Part::slotShowExtractionDialog); } menu->addSeparator(); QAction *header = menu->addAction(i18n("Quick Extract To...")); header->setEnabled(false); header->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract"))); } while (menu->actions().size() > 3) { menu->removeAction(menu->actions().last()); } const KConfigGroup conf(KSharedConfig::openConfig(), "ExtractDialog"); const QStringList dirHistory = conf.readPathEntry("DirHistory", QStringList()); for (int i = 0; i < qMin(10, dirHistory.size()); ++i) { const QString dir = QUrl(dirHistory.value(i)).toString(QUrl::RemoveScheme | QUrl::NormalizePathSegments | QUrl::PreferLocalFile); if (QDir(dir).exists()) { QAction *newAction = menu->addAction(dir); newAction->setData(dir); } } } void Part::slotQuickExtractFiles(QAction *triggeredAction) { // #190507: triggeredAction->data.isNull() means it's the "Extract to..." // action, and we do not want it to run here if (!triggeredAction->data().isNull()) { const QString userDestination = triggeredAction->data().toString(); qCDebug(ARK) << "Extract to user dest" << userDestination; QString finalDestinationDirectory; const QString detectedSubfolder = detectSubfolder(); qCDebug(ARK) << "Detected subfolder" << detectedSubfolder; if (!isSingleFolderArchive()) { finalDestinationDirectory = userDestination + QDir::separator() + detectedSubfolder; QDir(userDestination).mkdir(detectedSubfolder); } else { finalDestinationDirectory = userDestination; } qCDebug(ARK) << "Extract to final dest" << finalDestinationDirectory; Kerfuffle::ExtractionOptions options; options[QStringLiteral("PreservePaths")] = true; QList files = filesAndRootNodesForIndexes(m_view->selectionModel()->selectedRows()); ExtractJob *job = m_model->extractFiles(files, finalDestinationDirectory, options); registerJob(job); connect(job, &KJob::result, this, &Part::slotExtractionDone); job->start(); } } void Part::selectionChanged() { m_infoPanel->setIndexes(m_view->selectionModel()->selectedRows()); } bool Part::openFile() { qCDebug(ARK) << "Attempting to open archive" << localFilePath(); if (!isLocalFileValid()) { 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()); // Plugin loaded successfully. KJob *job = m_model->setArchive(archive.take()); if (job) { registerJob(job); job->start(); } else { updateActions(); } m_infoPanel->setIndex(QModelIndex()); if (arguments().metaData()[QStringLiteral("showExtractDialog")] == QLatin1String("true")) { QTimer::singleShot(0, this, &Part::slotShowExtractionDialog); } const QString password = arguments().metaData()[QStringLiteral("encryptionPassword")]; if (!password.isEmpty()) { m_model->encryptArchive(password, arguments().metaData()[QStringLiteral("encryptHeader")] == QLatin1String("true")); } return true; } bool Part::saveFile() { return true; } bool Part::isBusy() const { return m_busy; } KConfigSkeleton *Part::config() const { return ArkSettings::self(); } QList Part::settingsPages(QWidget *parent) const { QList pages; pages.append(new ExtractionSettingsPage(parent, i18nc("@title:tab", "Extraction Settings"), QStringLiteral("archive-extract"))); pages.append(new PreviewSettingsPage(parent, i18nc("@title:tab", "Preview Settings"), QStringLiteral("document-preview-archive"))); return pages; } bool Part::isLocalFileValid() { const QString localFile = localFilePath(); const QFileInfo localFileInfo(localFile); const bool creatingNewArchive = arguments().metaData()[QStringLiteral("createNewArchive")] == QLatin1String("true"); if (localFileInfo.isDir()) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "%1 is a directory.", localFile)); return false; } if (creatingNewArchive) { if (localFileInfo.exists()) { if (!confirmAndDelete(localFile)) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Could not overwrite %1. Check whether you have write permission.", localFile)); return false; } } displayMsgWidget(KMessageWidget::Information, xi18nc("@info", "The archive %1 will be created as soon as you add a file.", localFile)); } else { if (!localFileInfo.exists()) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "The archive %1 was not found.", localFile)); return false; } if (!localFileInfo.isReadable()) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "The archive %1 could not be loaded, as it was not possible to read from it.", localFile)); return false; } } return true; } bool Part::confirmAndDelete(const QString &targetFile) { QFileInfo targetInfo(targetFile); const auto buttonCode = KMessageBox::warningYesNo(widget(), xi18nc("@info", "The archive %1 already exists. Do you wish to overwrite it?", targetInfo.fileName()), i18nc("@title:window", "File Exists"), KGuiItem(i18nc("@action:button", "Overwrite")), KStandardGuiItem::cancel()); if (buttonCode != KMessageBox::Yes || !targetInfo.isWritable()) { return false; } qCDebug(ARK) << "Removing file" << targetFile; return QFile(targetFile).remove(); } void Part::slotLoadingStarted() { } void Part::slotLoadingFinished(KJob *job) { if (job->error()) { if (arguments().metaData()[QStringLiteral("createNewArchive")] != QLatin1String("true")) { if (job->error() != KJob::KilledJobError) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Loading the archive %1 failed with the following error:%2", localFilePath(), job->errorText())); } // The file failed to open, so reset the open archive, info panel and caption. m_model->setArchive(Q_NULLPTR); m_infoPanel->setPrettyFileName(QString()); m_infoPanel->updateWithDefaults(); emit setWindowCaption(QString()); } } m_view->sortByColumn(0, Qt::AscendingOrder); // #303708: expand the first level only when there is just one root folder. // Typical use case: an archive with source files. if (m_view->model()->rowCount() == 1) { m_view->expandToDepth(0); } // After loading all files, resize the columns to fit all fields m_view->header()->resizeSections(QHeaderView::ResizeToContents); updateActions(); if (!m_model->archive()) { return; } if (!m_model->archive()->comment().isEmpty()) { m_commentView->setPlainText(m_model->archive()->comment()); slotShowComment(); } else { m_commentView->clear(); m_commentBox->hide(); } if (m_model->rowCount() == 0) { qCWarning(ARK) << "No entry listed by the plugin"; displayMsgWidget(KMessageWidget::Warning, xi18nc("@info", "The archive is empty or Ark could not open its content.")); } else if (m_model->rowCount() == 1) { if (m_model->archive()->mimeType().inherits(QStringLiteral("application/x-cd-image")) && m_model->entryForIndex(m_model->index(0, 0))->property("fullPath").toString() == QLatin1String("README.TXT")) { qCWarning(ARK) << "Detected ISO image with UDF filesystem"; displayMsgWidget(KMessageWidget::Warning, xi18nc("@info", "Ark does not currently support ISO files with UDF filesystem.")); } } } void Part::setReadyGui() { QApplication::restoreOverrideCursor(); m_busy = false; if (m_statusBarExtension->statusBar()) { m_statusBarExtension->statusBar()->hide(); } m_view->setEnabled(true); updateActions(); } void Part::setBusyGui() { QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); m_busy = true; if (m_statusBarExtension->statusBar()) { m_statusBarExtension->statusBar()->show(); } m_view->setEnabled(false); updateActions(); } void Part::setFileNameFromArchive() { const QString prettyName = url().fileName(); m_infoPanel->setPrettyFileName(prettyName); m_infoPanel->updateWithDefaults(); emit setWindowCaption(prettyName); } void Part::slotOpenEntry(int mode) { qCDebug(ARK) << "Opening with mode" << mode; QModelIndex index = m_view->selectionModel()->currentIndex(); Archive::Entry *entry = m_model->entryForIndex(index); // Don't open directories. if (entry->isDir()) { return; } // We don't support opening symlinks. if (!entry->property("link").toString().isEmpty()) { displayMsgWidget(KMessageWidget::Information, i18n("Ark cannot open symlinks.")); return; } // Extract the entry. if (!entry->property("fullPath").toString().isEmpty()) { m_openFileMode = static_cast(mode); KJob *job = Q_NULLPTR; if (m_openFileMode == Preview) { job = m_model->preview(entry); connect(job, &KJob::result, this, &Part::slotPreviewExtractedEntry); } else { job = (m_openFileMode == OpenFile) ? m_model->open(entry) : m_model->openWith(entry); connect(job, &KJob::result, this, &Part::slotOpenExtractedEntry); } registerJob(job); job->start(); } } void Part::slotOpenExtractedEntry(KJob *job) { if (!job->error()) { OpenJob *openJob = qobject_cast(job); Q_ASSERT(openJob); // Since the user could modify the file (unlike the Preview case), // we'll need to manually delete the temp dir in the Part destructor. m_tmpOpenDirList << openJob->tempDir(); const QString fullName = openJob->validatedFilePath(); bool isWritable = m_model->archive() && !m_model->archive()->isReadOnly(); // If archive is readonly set temporarily extracted file to readonly as // well so user will be notified if trying to modify and save the file. if (!isWritable) { QFile::setPermissions(fullName, QFileDevice::ReadOwner | QFileDevice::ReadGroup | QFileDevice::ReadOther); } if (isWritable) { m_fileWatcher = new QFileSystemWatcher; connect(m_fileWatcher, &QFileSystemWatcher::fileChanged, this, &Part::slotWatchedFileModified); m_fileWatcher->addPath(fullName); } if (qobject_cast(job)) { const QList urls = {QUrl::fromUserInput(fullName, QString(), QUrl::AssumeLocalFile)}; KRun::displayOpenWithDialog(urls, widget()); } else { KRun::runUrl(QUrl::fromUserInput(fullName, QString(), QUrl::AssumeLocalFile), QMimeDatabase().mimeTypeForFile(fullName).name(), widget()); } } else if (job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } setReadyGui(); } void Part::slotPreviewExtractedEntry(KJob *job) { if (!job->error()) { PreviewJob *previewJob = qobject_cast(job); Q_ASSERT(previewJob); ArkViewer::view(previewJob->validatedFilePath()); } else if (job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } setReadyGui(); } void Part::slotWatchedFileModified(const QString& file) { qCDebug(ARK) << "Watched file modified:" << file; // Find the relative path of the file within the archive. QString relPath = file; foreach (QTemporaryDir *tmpDir, m_tmpOpenDirList) { relPath.remove(tmpDir->path()); //Remove tmpDir. } relPath = relPath.mid(1); //Remove leading slash. if (relPath.contains(QLatin1Char('/'))) { relPath = relPath.section(QLatin1Char('/'), 0, -2); //Remove filename. } else { // File is in the root of the archive, no path. relPath = QString(); } // Set up a string for display in KMessageBox. QString prettyFilename; if (relPath.isEmpty()) { prettyFilename = file.section(QLatin1Char('/'), -1); } else { prettyFilename = relPath + QLatin1Char('/') + file.section(QLatin1Char('/'), -1); } if (KMessageBox::questionYesNo(widget(), xi18n("The file %1 was modified. Do you want to update the archive?", prettyFilename), i18nc("@title:window", "File Modified")) == KMessageBox::Yes) { QStringList list = QStringList() << file; qCDebug(ARK) << "Updating file" << file << "with path" << relPath; slotAddFiles(list, relPath); } // This is needed because some apps, such as Kate, delete and recreate // files when saving. m_fileWatcher->addPath(file); } void Part::slotError(const QString& errorMessage, const QString& details) { if (details.isEmpty()) { KMessageBox::error(widget(), errorMessage); } else { KMessageBox::detailedError(widget(), errorMessage, details); } } bool Part::isSingleFolderArchive() const { return m_model->archive()->isSingleFolderArchive(); } QString Part::detectSubfolder() const { if (!m_model) { return QString(); } return m_model->archive()->subfolderName(); } void Part::slotExtractArchive() { if (m_view->selectionModel()->selectedRows().count() > 0) { m_view->selectionModel()->clear(); } slotShowExtractionDialog(); } void Part::slotShowExtractionDialog() { if (!m_model) { return; } QPointer dialog(new Kerfuffle::ExtractionDialog); dialog.data()->setModal(true); if (m_view->selectionModel()->selectedRows().count() > 0) { dialog.data()->setShowSelectedFiles(true); } dialog.data()->setSingleFolderArchive(isSingleFolderArchive()); dialog.data()->setSubfolder(detectSubfolder()); dialog.data()->setCurrentUrl(QUrl::fromLocalFile(QFileInfo(m_model->archive()->fileName()).absolutePath())); dialog.data()->show(); dialog.data()->restoreWindowSize(); if (dialog.data()->exec()) { updateQuickExtractMenu(m_extractArchiveAction); updateQuickExtractMenu(m_extractAction); QList files; // If the user has chosen to extract only selected entries, fetch these // from the QTreeView. if (!dialog.data()->extractAllFiles()) { files = filesAndRootNodesForIndexes(addChildren(m_view->selectionModel()->selectedRows())); } qCDebug(ARK) << "Selected " << files; Kerfuffle::ExtractionOptions options; if (dialog.data()->preservePaths()) { options[QStringLiteral("PreservePaths")] = true; } options[QStringLiteral("FollowExtractionDialogSettings")] = true; const QString destinationDirectory = dialog.data()->destinationDirectory().toDisplayString(QUrl::PreferLocalFile); ExtractJob *job = m_model->extractFiles(files, destinationDirectory, options); registerJob(job); connect(job, &KJob::result, this, &Part::slotExtractionDone); job->start(); } delete dialog.data(); } QModelIndexList Part::addChildren(const QModelIndexList &list) const { Q_ASSERT(m_model); QModelIndexList ret = list; // Iterate over indexes in list and add all children. for (int i = 0; i < ret.size(); ++i) { QModelIndex index = ret.at(i); for (int j = 0; j < m_model->rowCount(index); ++j) { QModelIndex child = m_model->index(j, 0, index); if (!ret.contains(child)) { ret << child; } } } return ret; } QList Part::filesForIndexes(const QModelIndexList& list) const { QList ret; foreach(const QModelIndex& index, list) { ret << m_model->entryForIndex(index); } return ret; } QList Part::filesAndRootNodesForIndexes(const QModelIndexList& list) const { QList fileList; foreach (const QModelIndex& index, list) { // Find the topmost unselected parent. This is done by iterating up // through the directory hierarchy and see if each parent is included // in the selection OR if the parent is already part of list. // The latter is needed for unselected folders which are subfolders of // a selected parent folder. QModelIndex selectionRoot = index.parent(); while (m_view->selectionModel()->isSelected(selectionRoot) || list.contains(selectionRoot)) { selectionRoot = selectionRoot.parent(); } // Fetch the root node for the unselected parent. const QString rootFileName = m_model->entryForIndex(selectionRoot)->property("fullPath").toString(); // Append index with root node to fileList. QModelIndexList alist = QModelIndexList() << index; foreach (Archive::Entry *entry, filesForIndexes(alist)) { if (!fileList.contains(entry)) { entry->rootNode = rootFileName; fileList.append(entry); } } } return fileList; } void Part::slotExtractionDone(KJob* job) { if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } else { ExtractJob *extractJob = qobject_cast(job); Q_ASSERT(extractJob); const bool followExtractionDialogSettings = extractJob->extractionOptions().value(QStringLiteral("FollowExtractionDialogSettings"), false).toBool(); if (!followExtractionDialogSettings) { return; } if (ArkSettings::openDestinationFolderAfterExtraction()) { qCDebug(ARK) << "Shall open" << extractJob->destinationDirectory(); QUrl destinationDirectory = QUrl::fromLocalFile(extractJob->destinationDirectory()).adjusted(QUrl::NormalizePathSegments); qCDebug(ARK) << "Shall open URL" << destinationDirectory; KRun::runUrl(destinationDirectory, QStringLiteral("inode/directory"), widget()); } if (ArkSettings::closeAfterExtraction()) { emit quit(); } } } void Part::adjustColumns() { m_view->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); } void Part::slotAddFiles(const QStringList& filesToAdd, const QString& path) { if (filesToAdd.isEmpty()) { return; } qCDebug(ARK) << "Adding " << filesToAdd << " to " << path; // Add a trailing slash to directories. QStringList cleanFilesToAdd(filesToAdd); for (int i = 0; i < cleanFilesToAdd.size(); ++i) { QString& file = cleanFilesToAdd[i]; if (QFileInfo(file).isDir()) { if (!file.endsWith(QLatin1Char( '/' ))) { file += QLatin1Char( '/' ); } } } // GlobalWorkDir is used by AddJob and should contain the part of the // absolute path of files to be added that should NOT be included in the // directory structure within the archive. // Example: We add file "/home/user/somedir/somefile.txt" and want the file // to have the relative path within the archive "somedir/somefile.txt". // GlobalWorkDir is then: "/home/user" QString globalWorkDir = cleanFilesToAdd.first(); // path represents the path of the file within the archive. This needs to // be removed from globalWorkDir, otherwise the files will be added to the // root of the archive. In the example above, path would be "somedir/". if (!path.isEmpty()) { globalWorkDir.remove(path); } // Remove trailing slash (needed when adding dirs). if (globalWorkDir.right(1) == QLatin1String("/")) { globalWorkDir.chop(1); } // Now take the absolute path of the parent directory. globalWorkDir = QFileInfo(globalWorkDir).dir().absolutePath(); qCDebug(ARK) << "Detected GlobalWorkDir to be " << globalWorkDir; CompressionOptions options; options[QStringLiteral("GlobalWorkDir")] = globalWorkDir; if (arguments().metaData().contains(QStringLiteral("compressionLevel"))) { options[QStringLiteral("CompressionLevel")] = arguments().metaData()[QStringLiteral("compressionLevel")]; } foreach (const QString& file, cleanFilesToAdd) { m_jobTempEntries.push_back(new Archive::Entry(Q_NULLPTR, file)); } - AddJob *job = m_model->addFiles(m_jobTempEntries, Q_NULLPTR, options); + AddJob *job = m_model->addFiles(m_jobTempEntries, options); if (!job) { return; } connect(job, &KJob::result, this, &Part::slotAddFilesDone); registerJob(job); job->start(); } void Part::slotAddFiles() { // #264819: passing widget() as the parent will not work as expected. // KFileDialog will create a KFileWidget, which runs an internal // event loop to stat the given directory. This, in turn, leads to // events being delivered to widget(), which is a QSplitter, which // in turn reimplements childEvent() and will end up calling // QWidget::show() on the KFileDialog (thus showing it in a // non-modal state). // When KFileDialog::exec() is called, the widget is already shown // and nothing happens. const QStringList filesToAdd = QFileDialog::getOpenFileNames(widget(), i18nc("@title:window", "Add Files")); slotAddFiles(filesToAdd); } void Part::slotAddDir() { const QString dirToAdd = QFileDialog::getExistingDirectory(widget(), i18nc("@title:window", "Add Folder")); if (!dirToAdd.isEmpty()) { slotAddFiles(QStringList() << dirToAdd); } } void Part::slotAddFilesDone(KJob* job) { qDeleteAll(m_jobTempEntries); m_jobTempEntries.clear(); if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } } void Part::slotDeleteFilesDone(KJob* job) { if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } } void Part::slotDeleteFiles() { const int selectionsCount = m_view->selectionModel()->selectedRows().count(); const auto reallyDelete = KMessageBox::questionYesNo(widget(), i18ncp("@info", "Deleting this file is not undoable. Are you sure you want to do this?", "Deleting these files is not undoable. Are you sure you want to do this?", selectionsCount), i18ncp("@title:window", "Delete File", "Delete Files", selectionsCount), KStandardGuiItem::del(), KStandardGuiItem::no(), QString(), KMessageBox::Dangerous | KMessageBox::Notify); if (reallyDelete == KMessageBox::No) { return; } DeleteJob *job = m_model->deleteFiles(filesForIndexes(addChildren(m_view->selectionModel()->selectedRows()))); connect(job, &KJob::result, this, &Part::slotDeleteFilesDone); registerJob(job); job->start(); } void Part::slotShowProperties() { QPointer dialog(new Kerfuffle::PropertiesDialog(0, m_model->archive())); dialog.data()->show(); } void Part::slotToggleInfoPanel(bool visible) { if (visible) { m_splitter->setSizes(ArkSettings::splitterSizes()); m_infoPanel->show(); } else { // We need to save the splitterSizes before hiding, otherwise // Ark won't remember resizing done by the user. ArkSettings::setSplitterSizes(m_splitter->sizes()); m_infoPanel->hide(); } } void Part::slotSaveAs() { QUrl saveUrl = QFileDialog::getSaveFileUrl(widget(), i18nc("@title:window", "Save Archive As"), url()); if ((saveUrl.isValid()) && (!saveUrl.isEmpty())) { auto statJob = KIO::stat(saveUrl, KIO::StatJob::DestinationSide, 0); KJobWidgets::setWindow(statJob, widget()); if (statJob->exec()) { int overwrite = KMessageBox::warningContinueCancel(widget(), xi18nc("@info", "An archive named %1 already exists. Are you sure you want to overwrite it?", saveUrl.fileName()), QString(), KStandardGuiItem::overwrite()); if (overwrite != KMessageBox::Continue) { return; } } QUrl srcUrl = QUrl::fromLocalFile(localFilePath()); if (!QFile::exists(localFilePath())) { if (url().isLocalFile()) { KMessageBox::error(widget(), xi18nc("@info", "The archive %1 cannot be copied to the specified location. The archive does not exist anymore.", localFilePath())); return; } else { srcUrl = url(); } } KIO::Job *copyJob = KIO::file_copy(srcUrl, saveUrl, -1, KIO::Overwrite); KJobWidgets::setWindow(copyJob, widget()); copyJob->exec(); if (copyJob->error()) { KMessageBox::error(widget(), xi18nc("@info", "The archive could not be saved as %1. Try saving it to another location.", saveUrl.path())); } } } void Part::slotShowContextMenu() { if (!factory()) { return; } QMenu *popup = static_cast(factory()->container(QStringLiteral("context_menu"), this)); popup->popup(QCursor::pos()); } void Part::displayMsgWidget(KMessageWidget::MessageType type, const QString& msg) { KMessageWidget *msgWidget = new KMessageWidget(); msgWidget->setText(msg); msgWidget->setMessageType(type); m_vlayout->insertWidget(0, msgWidget); msgWidget->animatedShow(); } } // namespace Ark #include "part.moc"