diff --git a/app/mainwindow.cpp b/app/mainwindow.cpp index 06b9000c..fe669848 100644 --- a/app/mainwindow.cpp +++ b/app/mainwindow.cpp @@ -1,354 +1,354 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2002-2003: Georg Robbers * Copyright (C) 2003: Helio Chissini de Castro * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008 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. * */ #include "mainwindow.h" #include "ark_debug.h" #include "archive_kerfuffle.h" #include "createdialog.h" #include "settingsdialog.h" #include "settingspage.h" #include "pluginmanager.h" #include "interface.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static bool isValidArchiveDrag(const QMimeData *data) { return ((data->hasUrls()) && (data->urls().count() == 1)); } MainWindow::MainWindow(QWidget *) : KParts::MainWindow() { setupActions(); setAcceptDrops(true); // Ark doesn't provide a fullscreen mode; remove the corresponding window button setWindowFlags(windowFlags() & ~Qt::WindowFullscreenButtonHint); } MainWindow::~MainWindow() { if (m_recentFilesAction) { m_recentFilesAction->saveEntries(KSharedConfig::openConfig()->group("Recent Files")); } guiFactory()->removeClient(m_part); delete m_part; m_part = nullptr; } void MainWindow::dragEnterEvent(QDragEnterEvent * event) { - qCDebug(ARK) << "dragEnterEvent" << event; + qCDebug(ARK) << event; Interface *iface = qobject_cast(m_part); if (iface->isBusy()) { return; } const bool partAcceptsDrops = !m_part->url().isEmpty() && m_part->isReadWrite(); if (!event->source() && isValidArchiveDrag(event->mimeData()) && !partAcceptsDrops) { event->acceptProposedAction(); } return; } void MainWindow::dropEvent(QDropEvent * event) { - qCDebug(ARK) << "dropEvent" << event; + qCDebug(ARK) << event; Interface *iface = qobject_cast(m_part); if (iface->isBusy()) { return; } if ((event->source() == nullptr) && (isValidArchiveDrag(event->mimeData()))) { event->acceptProposedAction(); } //TODO: if this call provokes a message box the drag will still be going //while the box is onscreen. looks buggy, do something about it openUrl(event->mimeData()->urls().at(0)); } void MainWindow::dragMoveEvent(QDragMoveEvent * event) { - qCDebug(ARK) << "dragMoveEvent" << event; + qCDebug(ARK) << event; Interface *iface = qobject_cast(m_part); if (iface->isBusy()) { return; } if ((event->source() == nullptr) && (isValidArchiveDrag(event->mimeData()))) { event->acceptProposedAction(); } } bool MainWindow::loadPart() { KPluginFactory *factory = nullptr; const auto plugins = KPluginLoader::findPlugins(QString(), [](const KPluginMetaData& metaData) { return metaData.pluginId() == QStringLiteral("arkpart") && metaData.serviceTypes().contains(QStringLiteral("KParts/ReadOnlyPart")) && metaData.serviceTypes().contains(QStringLiteral("Browser/View")); }); if (!plugins.isEmpty()) { factory = KPluginLoader(plugins.first().fileName()).factory(); } m_part = factory ? static_cast(factory->create(this)) : nullptr; if (!m_part) { KMessageBox::error(this, i18n("Unable to find Ark's KPart component, please check your installation.")); qCWarning(ARK) << "Error loading Ark KPart."; return false; } m_part->setObjectName(QStringLiteral("ArkPart")); setCentralWidget(m_part->widget()); setXMLFile(QStringLiteral("arkui.rc")); setupGUI(ToolBar | Keys | Save); createGUI(m_part); statusBar()->hide(); connect(m_part, SIGNAL(ready()), this, SLOT(updateActions())); connect(m_part, SIGNAL(quit()), this, SLOT(quit())); // #365200: this will disable m_recentFilesAction, while openUrl() will enable it. // So updateActions() needs to be called after openUrl() returns. connect(m_part, SIGNAL(busy()), this, SLOT(updateActions()), Qt::QueuedConnection); connect(m_part, QOverload<>::of(&KParts::ReadOnlyPart::completed), this, &MainWindow::addPartUrl); updateActions(); return true; } void MainWindow::setupActions() { m_newAction = KStandardAction::openNew(this, &MainWindow::newArchive, this); actionCollection()->addAction(QStringLiteral("ark_file_new"), m_newAction); m_openAction = KStandardAction::open(this, &MainWindow::openArchive, this); actionCollection()->addAction(QStringLiteral("ark_file_open"), m_openAction); auto quitAction = KStandardAction::quit(this, &MainWindow::quit, this); actionCollection()->addAction(QStringLiteral("ark_quit"), quitAction); m_recentFilesAction = KStandardAction::openRecent(this, &MainWindow::openUrl, this); actionCollection()->addAction(QStringLiteral("ark_file_open_recent"), m_recentFilesAction); m_recentFilesAction->setToolBarMode(KRecentFilesAction::MenuMode); m_recentFilesAction->setToolButtonPopupMode(QToolButton::DelayedPopup); m_recentFilesAction->setIconText(i18nc("action, to open an archive", "Open")); m_recentFilesAction->setToolTip(i18n("Open an archive")); m_recentFilesAction->loadEntries(KSharedConfig::openConfig()->group("Recent Files")); KStandardAction::preferences(this, &MainWindow::showSettings, actionCollection()); } void MainWindow::updateActions() { Interface *iface = qobject_cast(m_part); Kerfuffle::PluginManager pluginManager; m_newAction->setEnabled(!iface->isBusy() && !pluginManager.availableWritePlugins().isEmpty()); m_openAction->setEnabled(!iface->isBusy()); m_recentFilesAction->setEnabled(!iface->isBusy()); } void MainWindow::openArchive() { Interface *iface = qobject_cast(m_part); Q_ASSERT(iface); Q_UNUSED(iface); Kerfuffle::PluginManager pluginManager; auto dlg = new QFileDialog(this, i18nc("to open an archive", "Open Archive")); dlg->setMimeTypeFilters(pluginManager.supportedMimeTypes(Kerfuffle::PluginManager::SortByComment)); dlg->setFileMode(QFileDialog::ExistingFile); dlg->setAcceptMode(QFileDialog::AcceptOpen); connect(dlg, &QDialog::finished, this, [this, dlg](int result) { if (result == QDialog::Accepted) { openUrl(dlg->selectedUrls().at(0)); } dlg->deleteLater(); }); dlg->open(); } void MainWindow::openUrl(const QUrl& url) { if (url.isEmpty()) { return; } m_part->setArguments(m_openArgs); m_part->openUrl(url); } void MainWindow::setShowExtractDialog(bool option) { if (option) { m_openArgs.metaData()[QStringLiteral("showExtractDialog")] = QStringLiteral("true"); } else { m_openArgs.metaData().remove(QStringLiteral("showExtractDialog")); } } void MainWindow::closeEvent(QCloseEvent *event) { // Preview windows don't have a parent, so we need to manually close them. foreach (QWidget *widget, qApp->topLevelWidgets()) { if (widget->isVisible()) { widget->close(); } } KParts::MainWindow::closeEvent(event); } void MainWindow::quit() { close(); } void MainWindow::showSettings() { if (KConfigDialog::showDialog(QStringLiteral("settings"))) { return; } Interface *iface = qobject_cast(m_part); Q_ASSERT(iface); auto dialog = new Kerfuffle::SettingsDialog(this, QStringLiteral("settings"), iface->config()); foreach (Kerfuffle::SettingsPage *page, iface->settingsPages(this)) { dialog->addPage(page, page->name(), page->iconName()); connect(dialog, &KConfigDialog::settingsChanged, page, &Kerfuffle::SettingsPage::slotSettingsChanged); connect(dialog, &Kerfuffle::SettingsDialog::defaultsButtonClicked, page, &Kerfuffle::SettingsPage::slotDefaultsButtonClicked); } // Hide the icons list if only one page has been added. dialog->setFaceType(KPageDialog::Auto); dialog->setModal(true); connect(dialog, &KConfigDialog::settingsChanged, this, &MainWindow::writeSettings); connect(dialog, &KConfigDialog::settingsChanged, this, &MainWindow::updateActions); dialog->show(); } void MainWindow::writeSettings() { Interface *iface = qobject_cast(m_part); Q_ASSERT(iface); iface->config()->save(); } void MainWindow::addPartUrl() { m_recentFilesAction->addUrl(m_part->url()); } void MainWindow::newArchive() { qCDebug(ARK) << "Creating new archive"; Interface *iface = qobject_cast(m_part); Q_ASSERT(iface); Q_UNUSED(iface); QPointer dialog = new Kerfuffle::CreateDialog( nullptr, // parent i18n("Create New Archive"), // caption QUrl()); // startDir if (dialog.data()->exec()) { const QUrl saveFileUrl = dialog.data()->selectedUrl(); const QString password = dialog.data()->password(); const QString fixedMimeType = dialog.data()->currentMimeType().name(); qCDebug(ARK) << "CreateDialog returned URL:" << saveFileUrl.toString(); qCDebug(ARK) << "CreateDialog returned mime:" << fixedMimeType; m_openArgs.metaData()[QStringLiteral("createNewArchive")] = QStringLiteral("true"); m_openArgs.metaData()[QStringLiteral("fixedMimeType")] = fixedMimeType; if (dialog.data()->compressionLevel() > -1) { m_openArgs.metaData()[QStringLiteral("compressionLevel")] = QString::number(dialog.data()->compressionLevel()); } if (dialog.data()->volumeSize() > 0) { qCDebug(ARK) << "Setting volume size:" << QString::number(dialog.data()->volumeSize()); m_openArgs.metaData()[QStringLiteral("volumeSize")] = QString::number(dialog.data()->volumeSize()); } if (!dialog.data()->compressionMethod().isEmpty()) { m_openArgs.metaData()[QStringLiteral("compressionMethod")] = dialog.data()->compressionMethod(); } if (!dialog.data()->encryptionMethod().isEmpty()) { m_openArgs.metaData()[QStringLiteral("encryptionMethod")] = dialog.data()->encryptionMethod(); } m_openArgs.metaData()[QStringLiteral("encryptionPassword")] = password; if (dialog.data()->isHeaderEncryptionEnabled()) { m_openArgs.metaData()[QStringLiteral("encryptHeader")] = QStringLiteral("true"); } openUrl(saveFileUrl); m_openArgs.metaData().remove(QStringLiteral("showExtractDialog")); m_openArgs.metaData().remove(QStringLiteral("createNewArchive")); m_openArgs.metaData().remove(QStringLiteral("fixedMimeType")); m_openArgs.metaData().remove(QStringLiteral("compressionLevel")); m_openArgs.metaData().remove(QStringLiteral("encryptionPassword")); m_openArgs.metaData().remove(QStringLiteral("encryptHeader")); } delete dialog.data(); } diff --git a/kerfuffle/addtoarchive.cpp b/kerfuffle/addtoarchive.cpp index cfeba44c..85c05ea7 100644 --- a/kerfuffle/addtoarchive.cpp +++ b/kerfuffle/addtoarchive.cpp @@ -1,254 +1,254 @@ /* * 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 "archiveentry.h" #include "archive_kerfuffle.h" #include "ark_debug.h" #include "createdialog.h" #include "jobs.h" #include #include #include #include #include #include #include #include #include #include namespace Kerfuffle { AddToArchive::AddToArchive(QObject *parent) : KJob(parent) , m_changeToFirstPath(false) , m_enableHeaderEncryption(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.toLocalFile(); } 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( 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()); m_options.setCompressionLevel(dialog.data()->compressionLevel()); m_options.setCompressionMethod(dialog.data()->compressionMethod()); m_options.setEncryptionMethod(dialog.data()->encryptionMethod()); m_options.setVolumeSize(dialog.data()->volumeSize()); } delete dialog.data(); return ret; } bool AddToArchive::addInput(const QUrl &url) { Archive::Entry *entry = new Archive::Entry(this); entry->setFullPath(url.toLocalFile()); m_entries << entry; if (m_firstPath.isEmpty()) { QString firstEntry = url.toLocalFile(); m_firstPath = QFileInfo(firstEntry).dir().absolutePath(); } return true; } void AddToArchive::start() { qCDebug(ARK) << "Starting job"; QTimer::singleShot(0, this, &AddToArchive::slotStartJob); } bool AddToArchive::doKill() { return m_createJob && m_createJob->kill(); } void AddToArchive::slotStartJob() { if (m_entries.isEmpty()) { KMessageBox::error(nullptr, i18n("No input files were given.")); emitResult(); return; } if (m_filename.isEmpty()) { if (m_autoFilenameSuffix.isEmpty()) { KMessageBox::error(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; m_filename = finalName; } 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->fullPath())); } qCDebug(ARK) << "Setting GlobalWorkDir to " << stripDir.path(); m_options.setGlobalWorkDir(stripDir.path()); } m_createJob = Archive::create(m_filename, m_mimeType, m_entries, m_options, this); if (!m_password.isEmpty()) { m_createJob->enableEncryption(m_password, m_enableHeaderEncryption); } KIO::getJobTracker()->registerJob(m_createJob); connect(m_createJob, &KJob::result, this, &AddToArchive::slotFinished); m_createJob->start(); } void AddToArchive::slotFinished(KJob *job) { - qCDebug(ARK) << "AddToArchive job finished"; + qCDebug(ARK) << "job finished"; if (job->error() && !job->errorString().isEmpty()) { KMessageBox::error(nullptr, job->errorString()); } emitResult(); } QString AddToArchive::detectBaseName(const QVector &entries) const { QFileInfo fileInfo = QFileInfo(entries.first()->fullPath()); 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/cliinterface.cpp b/kerfuffle/cliinterface.cpp index 6a13188d..7ef333d4 100644 --- a/kerfuffle/cliinterface.cpp +++ b/kerfuffle/cliinterface.cpp @@ -1,1111 +1,1111 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "cliinterface.h" #include "ark_debug.h" #include "queries.h" #ifdef Q_OS_WIN # include #else # include # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Kerfuffle { CliInterface::CliInterface(QObject *parent, const QVariantList & args) : ReadWriteArchiveInterface(parent, args) { //because this interface uses the event loop setWaitForFinishedSignal(true); if (QMetaType::type("QProcess::ExitStatus") == 0) { qRegisterMetaType("QProcess::ExitStatus"); } m_cliProps = new CliProperties(this, m_metaData, mimetype()); } CliInterface::~CliInterface() { Q_ASSERT(!m_process); } void CliInterface::setListEmptyLines(bool emptyLines) { m_listEmptyLines = emptyLines; } int CliInterface::copyRequiredSignals() const { return 2; } bool CliInterface::list() { resetParsing(); m_operationMode = List; m_numberOfEntries = 0; // To compute progress. m_archiveSizeOnDisk = static_cast(QFileInfo(filename()).size()); connect(this, &ReadOnlyArchiveInterface::entry, this, &CliInterface::onEntry); return runProcess(m_cliProps->property("listProgram").toString(), m_cliProps->listArgs(filename(), password())); } bool CliInterface::extractFiles(const QVector &files, const QString &destinationDirectory, const ExtractionOptions &options) { - qCDebug(ARK) << Q_FUNC_INFO << "to" << destinationDirectory; + qCDebug(ARK) << "destination directory:" << destinationDirectory; m_operationMode = Extract; m_extractionOptions = options; m_extractedFiles = files; m_extractDestDir = destinationDirectory; if (!m_cliProps->property("passwordSwitch").toStringList().isEmpty() && options.encryptedArchiveHint() && password().isEmpty()) { qCDebug(ARK) << "Password hint enabled, querying user"; if (!passwordQuery()) { return false; } } QUrl destDir = QUrl(destinationDirectory); m_oldWorkingDirExtraction = QDir::currentPath(); QDir::setCurrent(destDir.adjusted(QUrl::RemoveScheme).url()); const bool useTmpExtractDir = options.isDragAndDropEnabled() || options.alwaysUseTempDir(); if (useTmpExtractDir) { // Create an hidden temp folder in the current directory. m_extractTempDir.reset(new QTemporaryDir(QStringLiteral(".%1-").arg(QCoreApplication::applicationName()))); qCDebug(ARK) << "Using temporary extraction dir:" << m_extractTempDir->path(); if (!m_extractTempDir->isValid()) { qCDebug(ARK) << "Creation of temporary directory failed."; emit finished(false); return false; } destDir = QUrl(m_extractTempDir->path()); QDir::setCurrent(destDir.adjusted(QUrl::RemoveScheme).url()); } return runProcess(m_cliProps->property("extractProgram").toString(), m_cliProps->extractArgs(filename(), extractFilesList(files), options.preservePaths(), password())); } bool CliInterface::addFiles(const QVector &files, const Archive::Entry *destination, const CompressionOptions& options, uint numberOfEntriesToAdd) { Q_UNUSED(numberOfEntriesToAdd) m_operationMode = Add; QVector filesToPass = QVector(); // If destination path is specified, we have recreate its structure inside the temp directory // and then place symlinks of targeted files there. const QString destinationPath = (destination == nullptr) ? QString() : destination->fullPath(); qCDebug(ARK) << "Adding" << files.count() << "file(s) to destination:" << destinationPath; if (!destinationPath.isEmpty()) { m_extractTempDir.reset(new QTemporaryDir()); const QString absoluteDestinationPath = m_extractTempDir->path() + QLatin1Char('/') + destinationPath; QDir qDir; qDir.mkpath(absoluteDestinationPath); QObject *preservedParent = nullptr; foreach (Archive::Entry *file, files) { // The entries may have parent. We have to save and apply it to our new entry in order to prevent memory // leaks. if (preservedParent == nullptr) { preservedParent = file->parent(); } const QString filePath = QDir::currentPath() + QLatin1Char('/') + file->fullPath(NoTrailingSlash); const QString newFilePath = absoluteDestinationPath + file->fullPath(NoTrailingSlash); if (QFile::link(filePath, newFilePath)) { qCDebug(ARK) << "Symlink's created:" << filePath << newFilePath; } else { qCDebug(ARK) << "Can't create symlink" << filePath << newFilePath; emit finished(false); return false; } } qCDebug(ARK) << "Changing working dir again to " << m_extractTempDir->path(); QDir::setCurrent(m_extractTempDir->path()); filesToPass.push_back(new Archive::Entry(preservedParent, destinationPath.split(QLatin1Char('/'), QString::SkipEmptyParts).at(0))); } else { filesToPass = files; } if (!m_cliProps->property("passwordSwitch").toString().isEmpty() && options.encryptedArchiveHint() && password().isEmpty()) { qCDebug(ARK) << "Password hint enabled, querying user"; if (!passwordQuery()) { return false; } } return runProcess(m_cliProps->property("addProgram").toString(), m_cliProps->addArgs(filename(), entryFullPaths(filesToPass, NoTrailingSlash), password(), isHeaderEncryptionEnabled(), options.compressionLevel(), options.compressionMethod(), options.encryptionMethod(), options.volumeSize())); } bool CliInterface::moveFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) { Q_UNUSED(options); m_operationMode = Move; m_removedFiles = files; QVector withoutChildren = entriesWithoutChildren(files); setNewMovedFiles(files, destination, withoutChildren.count()); return runProcess(m_cliProps->property("moveProgram").toString(), m_cliProps->moveArgs(filename(), withoutChildren, destination, password())); } bool CliInterface::copyFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) { m_oldWorkingDir = QDir::currentPath(); m_tempWorkingDir.reset(new QTemporaryDir()); m_tempAddDir.reset(new QTemporaryDir()); QDir::setCurrent(m_tempWorkingDir->path()); m_passedFiles = files; m_passedDestination = destination; m_passedOptions = options; m_numberOfEntries = 0; m_subOperation = Extract; connect(this, &CliInterface::finished, this, &CliInterface::continueCopying); return extractFiles(files, QDir::currentPath(), ExtractionOptions()); } bool CliInterface::deleteFiles(const QVector &files) { m_operationMode = Delete; m_removedFiles = files; return runProcess(m_cliProps->property("deleteProgram").toString(), m_cliProps->deleteArgs(filename(), files, password())); } bool CliInterface::testArchive() { resetParsing(); m_operationMode = Test; return runProcess(m_cliProps->property("testProgram").toString(), m_cliProps->testArgs(filename(), password())); } bool CliInterface::runProcess(const QString& programName, const QStringList& arguments) { Q_ASSERT(!m_process); QString programPath = QStandardPaths::findExecutable(programName); if (programPath.isEmpty()) { emit error(xi18nc("@info", "Failed to locate program %1 on disk.", programName)); emit finished(false); return false; } qCDebug(ARK) << "Executing" << programPath << arguments << "within directory" << QDir::currentPath(); #ifdef Q_OS_WIN m_process = new KProcess; #else m_process = new KPtyProcess; m_process->setPtyChannels(KPtyProcess::StdinChannel); #endif m_process->setOutputChannelMode(KProcess::MergedChannels); m_process->setNextOpenMode(QIODevice::ReadWrite | QIODevice::Unbuffered | QIODevice::Text); m_process->setProgram(programPath, arguments); connect(m_process, &QProcess::readyReadStandardOutput, this, [=]() { readStdout(); }); if (m_operationMode == Extract) { // Extraction jobs need a dedicated post-processing function. connect(m_process, QOverload::of(&QProcess::finished), this, &CliInterface::extractProcessFinished); } else { connect(m_process, QOverload::of(&QProcess::finished), this, &CliInterface::processFinished); } m_stdOutData.clear(); m_process->start(); return true; } void CliInterface::processFinished(int exitCode, QProcess::ExitStatus exitStatus) { m_exitCode = exitCode; qCDebug(ARK) << "Process finished, exitcode:" << exitCode << "exitstatus:" << exitStatus; if (m_process) { //handle all the remaining data in the process readStdout(true); delete m_process; m_process = nullptr; } // #193908 - #222392 // Don't emit finished() if the job was killed quietly. if (m_abortingOperation) { return; } if (m_operationMode == Delete || m_operationMode == Move) { QStringList removedFullPaths = entryFullPaths(m_removedFiles); foreach (const QString &fullPath, removedFullPaths) { emit entryRemoved(fullPath); } foreach (Archive::Entry *e, m_newMovedFiles) { emit entry(e); } m_newMovedFiles.clear(); } if (m_operationMode == Add && !isMultiVolume()) { list(); } else if (m_operationMode == List && isCorrupt()) { Kerfuffle::LoadCorruptQuery query(filename()); query.execute(); if (!query.responseYes()) { emit cancelled(); emit finished(false); } else { emit progress(1.0); emit finished(true); } } else { emit progress(1.0); emit finished(true); } } void CliInterface::extractProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) { Q_ASSERT(m_operationMode == Extract); m_exitCode = exitCode; qCDebug(ARK) << "Extraction process finished, exitcode:" << exitCode << "exitstatus:" << exitStatus; if (m_process) { // Handle all the remaining data in the process. readStdout(true); delete m_process; m_process = nullptr; } // Don't emit finished() if the job was killed quietly. if (m_abortingOperation) { return; } if (m_extractionOptions.alwaysUseTempDir()) { // unar exits with code 1 if extraction fails. // This happens at least with wrong passwords or not enough space in the destination folder. if (m_exitCode == 1) { if (password().isEmpty()) { qCWarning(ARK) << "Extraction aborted, destination folder might not have enough space."; emit error(i18n("Extraction failed. Make sure that enough space is available.")); } else { qCWarning(ARK) << "Extraction aborted, either the password is wrong or the destination folder doesn't have enough space."; emit error(i18n("Extraction failed. Make sure you provided the correct password and that enough space is available.")); setPassword(QString()); } cleanUpExtracting(); emit finished(false); return; } if (!m_extractionOptions.isDragAndDropEnabled()) { if (!moveToDestination(QDir::current(), QDir(m_extractDestDir), m_extractionOptions.preservePaths())) { emit error(i18ncp("@info", "Could not move the extracted file to the destination directory.", "Could not move the extracted files to the destination directory.", m_extractedFiles.size())); cleanUpExtracting(); emit finished(false); return; } cleanUpExtracting(); } } if (m_extractionOptions.isDragAndDropEnabled()) { if (!moveDroppedFilesToDest(m_extractedFiles, m_extractDestDir)) { // FIXME: if the user canceled the overwrite query, we should emit cancelled(), not error(). emit error(i18ncp("@info", "Could not move the extracted file to the destination directory.", "Could not move the extracted files to the destination directory.", m_extractedFiles.size())); cleanUpExtracting(); emit finished(false); return; } cleanUpExtracting(); } // #395939: make sure we *always* restore the old working dir. restoreWorkingDirExtraction(); emit progress(1.0); emit finished(true); } void CliInterface::continueCopying(bool result) { if (!result) { finishCopying(false); return; } switch (m_subOperation) { case Extract: m_subOperation = Add; m_passedFiles = entriesWithoutChildren(m_passedFiles); if (!setAddedFiles() || !addFiles(m_tempAddedFiles, m_passedDestination, m_passedOptions)) { finishCopying(false); } break; case Add: finishCopying(true); break; default: Q_ASSERT(false); } } bool CliInterface::moveDroppedFilesToDest(const QVector &files, const QString &finalDest) { // Move extracted files from a QTemporaryDir to the final destination. QDir finalDestDir(finalDest); qCDebug(ARK) << "Setting final dir to" << finalDest; bool overwriteAll = false; bool skipAll = false; foreach (const Archive::Entry *file, files) { QFileInfo relEntry(file->fullPath().remove(file->rootNode)); QFileInfo absSourceEntry(QDir::current().absolutePath() + QLatin1Char('/') + file->fullPath()); QFileInfo absDestEntry(finalDestDir.path() + QLatin1Char('/') + relEntry.filePath()); if (absSourceEntry.isDir()) { // For directories, just create the path. if (!finalDestDir.mkpath(relEntry.filePath())) { qCWarning(ARK) << "Failed to create directory" << relEntry.filePath() << "in final destination."; } } else { // If destination file exists, prompt the user. if (absDestEntry.exists()) { qCWarning(ARK) << "File" << absDestEntry.absoluteFilePath() << "exists."; if (!skipAll && !overwriteAll) { Kerfuffle::OverwriteQuery query(absDestEntry.absoluteFilePath()); query.setNoRenameMode(true); query.execute(); if (query.responseOverwrite() || query.responseOverwriteAll()) { if (query.responseOverwriteAll()) { overwriteAll = true; } if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } else if (query.responseSkip() || query.responseAutoSkip()) { if (query.responseAutoSkip()) { skipAll = true; } continue; } else if (query.responseCancelled()) { qCDebug(ARK) << "Copy action cancelled."; return false; } } else if (skipAll) { continue; } else if (overwriteAll) { if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } } // Create any parent directories. if (!finalDestDir.mkpath(relEntry.path())) { qCWarning(ARK) << "Failed to create parent directory for file:" << absDestEntry.filePath(); } // Move files to the final destination. if (!QFile(absSourceEntry.absoluteFilePath()).rename(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to move file" << absSourceEntry.filePath() << "to final destination."; return false; } } } return true; } bool CliInterface::isEmptyDir(const QDir &dir) { QDir d = dir; d.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot); return d.count() == 0; } void CliInterface::cleanUpExtracting() { restoreWorkingDirExtraction(); m_extractTempDir.reset(); } void CliInterface::restoreWorkingDirExtraction() { if (m_oldWorkingDirExtraction.isEmpty()) { return; } if (!QDir::setCurrent(m_oldWorkingDirExtraction)) { qCWarning(ARK) << "Failed to restore old working directory:" << m_oldWorkingDirExtraction; } else { m_oldWorkingDirExtraction.clear(); } } void CliInterface::finishCopying(bool result) { disconnect(this, &CliInterface::finished, this, &CliInterface::continueCopying); emit progress(1.0); emit finished(result); cleanUp(); } bool CliInterface::moveToDestination(const QDir &tempDir, const QDir &destDir, bool preservePaths) { qCDebug(ARK) << "Moving extracted files from temp dir" << tempDir.path() << "to final destination" << destDir.path(); bool overwriteAll = false; bool skipAll = false; QDirIterator dirIt(tempDir.path(), QDir::AllEntries | QDir::Hidden | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (dirIt.hasNext()) { dirIt.next(); // We skip directories if: // 1. We are not preserving paths // 2. The dir is not empty. Only empty directories need to be explicitly moved. // The non-empty ones are created by QDir::mkpath() below. if (dirIt.fileInfo().isDir()) { if (!preservePaths || !isEmptyDir(QDir(dirIt.filePath()))) { continue; } } QFileInfo relEntry; if (preservePaths) { relEntry = QFileInfo(dirIt.filePath().remove(tempDir.path() + QLatin1Char('/'))); } else { relEntry = QFileInfo(dirIt.fileName()); } QFileInfo absDestEntry(destDir.path() + QLatin1Char('/') + relEntry.filePath()); if (absDestEntry.exists()) { qCWarning(ARK) << "File" << absDestEntry.absoluteFilePath() << "exists."; Kerfuffle::OverwriteQuery query(absDestEntry.absoluteFilePath()); query.setNoRenameMode(true); query.execute(); if (query.responseOverwrite() || query.responseOverwriteAll()) { if (query.responseOverwriteAll()) { overwriteAll = true; } if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } else if (query.responseSkip() || query.responseAutoSkip()) { if (query.responseAutoSkip()) { skipAll = true; } continue; } else if (query.responseCancelled()) { qCDebug(ARK) << "Copy action cancelled."; return false; } } else if (skipAll) { continue; } else if (overwriteAll) { if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } if (preservePaths) { // Create any parent directories. if (!destDir.mkpath(relEntry.path())) { qCWarning(ARK) << "Failed to create parent directory for file:" << absDestEntry.filePath(); } } // Move file to the final destination. if (!QFile(dirIt.filePath()).rename(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to move file" << dirIt.filePath() << "to final destination."; return false; } } return true; } void CliInterface::setNewMovedFiles(const QVector &entries, const Archive::Entry *destination, int entriesWithoutChildren) { m_newMovedFiles.clear(); QMap entryMap; foreach (const Archive::Entry* entry, entries) { entryMap.insert(entry->fullPath(), entry); } QString lastFolder; QString newPath; int nameLength = 0; foreach (const Archive::Entry* entry, entryMap) { if (lastFolder.count() > 0 && entry->fullPath().startsWith(lastFolder)) { // Replace last moved or copied folder path with destination path. int charsCount = entry->fullPath().count() - lastFolder.count(); if (entriesWithoutChildren > 1) { charsCount += nameLength; } newPath = destination->fullPath() + entry->fullPath().right(charsCount); } else { if (entriesWithoutChildren > 1) { newPath = destination->fullPath() + entry->name(); } else { // If there is only one passed file in the list, // we have to use destination as newPath. newPath = destination->fullPath(NoTrailingSlash); } if (entry->isDir()) { newPath += QLatin1Char('/'); nameLength = entry->name().count() + 1; // plus slash lastFolder = entry->fullPath(); } else { nameLength = 0; lastFolder = QString(); } } Archive::Entry *newEntry = new Archive::Entry(nullptr); newEntry->copyMetaData(entry); newEntry->setFullPath(newPath); m_newMovedFiles << newEntry; } } QStringList CliInterface::extractFilesList(const QVector &entries) const { QStringList filesList; foreach (const Archive::Entry *e, entries) { filesList << escapeFileName(e->fullPath(NoTrailingSlash)); } return filesList; } void CliInterface::killProcess(bool emitFinished) { // TODO: Would be good to unit test #304764/#304178. if (!m_process) { return; } m_abortingOperation = !emitFinished; // Give some time for the application to finish gracefully if (!m_process->waitForFinished(5)) { m_process->kill(); // It takes a few hundred ms for the process to be killed. m_process->waitForFinished(1000); } m_abortingOperation = false; } bool CliInterface::passwordQuery() { Kerfuffle::PasswordNeededQuery query(filename()); query.execute(); if (query.responseCancelled()) { emit cancelled(); // There is no process running, so finished() must be emitted manually. emit finished(false); return false; } setPassword(query.password()); return true; } void CliInterface::cleanUp() { qDeleteAll(m_tempAddedFiles); m_tempAddedFiles.clear(); QDir::setCurrent(m_oldWorkingDir); m_tempWorkingDir.reset(); m_tempAddDir.reset(); } void CliInterface::readStdout(bool handleAll) { //when hacking this function, please remember the following: //- standard output comes in unpredictable chunks, this is why //you can never know if the last part of the output is a complete line or not //- console applications are not really consistent about what //characters they send out (newline, backspace, carriage return, //etc), so keep in mind that this function is supposed to handle //all those special cases and be the lowest common denominator if (m_abortingOperation) return; Q_ASSERT(m_process); if (!m_process->bytesAvailable()) { //if process has no more data, we can just bail out return; } QByteArray dd = m_process->readAllStandardOutput(); m_stdOutData += dd; QList lines = m_stdOutData.split('\n'); //The reason for this check is that archivers often do not end //queries (such as file exists, wrong password) on a new line, but //freeze waiting for input. So we check for errors on the last line in //all cases. // TODO: QLatin1String() might not be the best choice here. // The call to handleLine() at the end of the method uses // QString::fromLocal8Bit(), for example. // TODO: The same check methods are called in handleLine(), this // is suboptimal. bool wrongPasswordMessage = m_cliProps->isWrongPasswordMsg(QLatin1String(lines.last())); bool foundErrorMessage = (wrongPasswordMessage || m_cliProps->isDiskFullMsg(QLatin1String(lines.last())) || m_cliProps->isfileExistsMsg(QLatin1String(lines.last()))) || m_cliProps->isPasswordPrompt(QLatin1String(lines.last())); if (foundErrorMessage) { handleAll = true; } if (wrongPasswordMessage) { setPassword(QString()); } //this is complex, here's an explanation: //if there is no newline, then there is no guaranteed full line to //handle in the output. The exception is that it is supposed to handle //all the data, OR if there's been an error message found in the //partial data. if (lines.size() == 1 && !handleAll) { return; } if (handleAll) { m_stdOutData.clear(); } else { //because the last line might be incomplete we leave it for now //note, this last line may be an empty string if the stdoutdata ends //with a newline m_stdOutData = lines.takeLast(); } foreach(const QByteArray& line, lines) { if (!line.isEmpty() || (m_listEmptyLines && m_operationMode == List)) { if (!handleLine(QString::fromLocal8Bit(line))) { killProcess(); return; } } } } bool CliInterface::setAddedFiles() { QDir::setCurrent(m_tempAddDir->path()); foreach (const Archive::Entry *file, m_passedFiles) { const QString oldPath = m_tempWorkingDir->path() + QLatin1Char('/') + file->fullPath(NoTrailingSlash); const QString newPath = m_tempAddDir->path() + QLatin1Char('/') + file->name(); if (!QFile::rename(oldPath, newPath)) { return false; } m_tempAddedFiles << new Archive::Entry(nullptr, file->name()); } return true; } bool CliInterface::handleLine(const QString& line) { // TODO: This should be implemented by each plugin; the way progress is // shown by each CLI application is subject to a lot of variation. if ((m_operationMode == Extract || m_operationMode == Add) && m_cliProps->property("captureProgress").toBool()) { //read the percentage int pos = line.indexOf(QLatin1Char( '%' )); if (pos > 1) { int percentage = line.midRef(pos - 2, 2).toInt(); emit progress(float(percentage) / 100); return true; } } if (m_operationMode == Extract) { if (m_cliProps->isPasswordPrompt(line)) { qCDebug(ARK) << "Found a password prompt"; Kerfuffle::PasswordNeededQuery query(filename()); query.execute(); if (query.responseCancelled()) { emit cancelled(); return false; } setPassword(query.password()); const QString response(password() + QLatin1Char('\n')); writeToProcess(response.toLocal8Bit()); return true; } if (m_cliProps->isDiskFullMsg(line)) { qCWarning(ARK) << "Found disk full message:" << line; emit error(i18nc("@info", "Extraction failed because the disk is full.")); return false; } if (m_cliProps->isWrongPasswordMsg(line)) { qCWarning(ARK) << "Wrong password!"; setPassword(QString()); emit error(i18nc("@info", "Extraction failed: Incorrect password")); return false; } if (handleFileExistsMessage(line)) { return true; } return readExtractLine(line); } if (m_operationMode == List) { if (m_cliProps->isPasswordPrompt(line)) { qCDebug(ARK) << "Found a password prompt"; Kerfuffle::PasswordNeededQuery query(filename()); query.execute(); if (query.responseCancelled()) { emit cancelled(); return false; } setPassword(query.password()); const QString response(password() + QLatin1Char('\n')); writeToProcess(response.toLocal8Bit()); return true; } if (m_cliProps->isWrongPasswordMsg(line)) { qCWarning(ARK) << "Wrong password!"; setPassword(QString()); emit error(i18n("Incorrect password.")); return false; } if (m_cliProps->isCorruptArchiveMsg(line)) { qCWarning(ARK) << "Archive corrupt"; setCorrupt(true); // Special case: corrupt is not a "fatal" error so we return true here. return true; } if (handleFileExistsMessage(line)) { return true; } return readListLine(line); } if (m_operationMode == Delete) { return readDeleteLine(line); } if (m_operationMode == Test) { if (m_cliProps->isPasswordPrompt(line)) { qCDebug(ARK) << "Found a password prompt"; emit error(i18n("Ark does not currently support testing this archive.")); return false; } if (m_cliProps->isTestPassedMsg(line)) { qCDebug(ARK) << "Test successful"; emit testSuccess(); return true; } } return true; } bool CliInterface::readDeleteLine(const QString &line) { Q_UNUSED(line); return true; } bool CliInterface::handleFileExistsMessage(const QString& line) { // Check for a filename and store it. foreach (const QString &pattern, m_cliProps->property("fileExistsFileName").toStringList()) { const QRegularExpression rxFileNamePattern(pattern); const QRegularExpressionMatch rxMatch = rxFileNamePattern.match(line); if (rxMatch.hasMatch()) { m_storedFileName = rxMatch.captured(1); qCWarning(ARK) << "Detected existing file:" << m_storedFileName; } } if (!m_cliProps->isfileExistsMsg(line)) { return false; } Kerfuffle::OverwriteQuery query(QDir::current().path() + QLatin1Char( '/' ) + m_storedFileName); query.setNoRenameMode(true); query.execute(); QString responseToProcess; const QStringList choices = m_cliProps->property("fileExistsInput").toStringList(); if (query.responseOverwrite()) { responseToProcess = choices.at(0); } else if (query.responseSkip()) { responseToProcess = choices.at(1); } else if (query.responseOverwriteAll()) { responseToProcess = choices.at(2); } else if (query.responseAutoSkip()) { responseToProcess = choices.at(3); } else if (query.responseCancelled()) { emit cancelled(); if (choices.count() < 5) { // If the program has no way to cancel the extraction, we resort to killing it return doKill(); } responseToProcess = choices.at(4); } Q_ASSERT(!responseToProcess.isEmpty()); responseToProcess += QLatin1Char( '\n' ); writeToProcess(responseToProcess.toLocal8Bit()); return true; } bool CliInterface::doKill() { if (m_process) { killProcess(false); return true; } return false; } QString CliInterface::escapeFileName(const QString& fileName) const { return fileName; } QStringList CliInterface::entryPathDestinationPairs(const QVector &entriesWithoutChildren, const Archive::Entry *destination) { QStringList pairList; if (entriesWithoutChildren.count() > 1) { foreach (const Archive::Entry *file, entriesWithoutChildren) { pairList << file->fullPath(NoTrailingSlash) << destination->fullPath() + file->name(); } } else { pairList << entriesWithoutChildren.at(0)->fullPath(NoTrailingSlash) << destination->fullPath(NoTrailingSlash); } return pairList; } void CliInterface::writeToProcess(const QByteArray& data) { Q_ASSERT(m_process); Q_ASSERT(!data.isNull()); qCDebug(ARK) << "Writing" << data << "to the process"; #ifdef Q_OS_WIN m_process->write(data); #else m_process->pty()->write(data); #endif } bool CliInterface::addComment(const QString &comment) { m_operationMode = Comment; m_commentTempFile.reset(new QTemporaryFile()); if (!m_commentTempFile->open()) { qCWarning(ARK) << "Failed to create temporary file for comment"; emit finished(false); return false; } QTextStream stream(m_commentTempFile.data()); stream << comment << endl; m_commentTempFile->close(); if (!runProcess(m_cliProps->property("addProgram").toString(), m_cliProps->commentArgs(filename(), m_commentTempFile->fileName()))) { return false; } m_comment = comment; return true; } QString CliInterface::multiVolumeName() const { QString oldSuffix = QMimeDatabase().suffixForFileName(filename()); QString name; foreach (const QString &multiSuffix, m_cliProps->property("multiVolumeSuffix").toStringList()) { QString newSuffix = multiSuffix; newSuffix.replace(QStringLiteral("$Suffix"), oldSuffix); name = filename().remove(oldSuffix).append(newSuffix); if (QFileInfo::exists(name)) { break; } } return name; } CliProperties *CliInterface::cliProperties() const { return m_cliProps; } void CliInterface::onEntry(Archive::Entry *archiveEntry) { if (archiveEntry->compressedSizeIsSet) { m_listedSize += archiveEntry->property("compressedSize").toULongLong(); if (m_listedSize <= m_archiveSizeOnDisk) { emit progress(float(m_listedSize)/float(m_archiveSizeOnDisk)); } else { // In case summed compressed size exceeds archive size on disk. emit progress(1); } } } } diff --git a/kerfuffle/createdialog.cpp b/kerfuffle/createdialog.cpp index 4a83568f..eb52f88f 100644 --- a/kerfuffle/createdialog.cpp +++ b/kerfuffle/createdialog.cpp @@ -1,252 +1,250 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2008 Harald Hvaal * Copyright (C) 2009,2011 Raphael Kubo da Costa * Copyright (C) 2015 Elvis Angelaccio * Copyright (C) 2016 Ragnar Thomsen * * 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 "createdialog.h" #include "archiveformat.h" #include "ark_debug.h" #include "ui_createdialog.h" #include "mimetypes.h" #include #include #include #include #include #include namespace Kerfuffle { class CreateDialogUI: public QWidget, public Ui::CreateDialog { Q_OBJECT public: CreateDialogUI(QWidget *parent = nullptr) : QWidget(parent) { setupUi(this); } }; CreateDialog::CreateDialog(QWidget *parent, const QString &caption, const QUrl &startDir) : QDialog(parent, Qt::Dialog) { - qCDebug(ARK) << "CreateDialog loaded"; - setWindowTitle(caption); setModal(true); m_supportedMimeTypes = m_pluginManger.supportedWriteMimeTypes(PluginManager::SortByComment); m_vlayout = new QVBoxLayout(); setLayout(m_vlayout); m_ui = new CreateDialogUI(this); m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); m_ui->destFolderUrlRequester->setMode(KFile::Directory); if (startDir.isEmpty()) { m_ui->destFolderUrlRequester->setUrl(QUrl::fromLocalFile(QDir::currentPath() + QLatin1Char('/'))); } else { m_ui->destFolderUrlRequester->setUrl(startDir); } // Populate combobox with mimetypes. foreach (const QString &type, m_supportedMimeTypes) { m_ui->mimeComboBox->addItem(QMimeDatabase().mimeTypeForName(type).comment()); } connect(m_ui->filenameLineEdit, &QLineEdit::textChanged, this, &CreateDialog::slotFileNameEdited); connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(this, &QDialog::accepted, this, &CreateDialog::slotUpdateDefaultMimeType); connect(m_ui->mimeComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &CreateDialog::slotUpdateWidgets); connect(m_ui->mimeComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &CreateDialog::slotUpdateFilenameExtension); m_vlayout->addWidget(m_ui); m_ui->optionsWidget->setMimeType(currentMimeType()); loadConfiguration(); layout()->setSizeConstraint(QLayout::SetFixedSize); m_ui->filenameLineEdit->setFocus(); slotUpdateFilenameExtension(m_ui->mimeComboBox->currentIndex()); } void CreateDialog::slotFileNameEdited(const QString &fileName) { const QMimeType mimeFromFileName = QMimeDatabase().mimeTypeForFile(fileName, QMimeDatabase::MatchExtension); if (m_supportedMimeTypes.contains(mimeFromFileName.name())) { setMimeType(mimeFromFileName.name()); } m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!fileName.isEmpty()); } void CreateDialog::slotUpdateWidgets(int index) { m_ui->optionsWidget->setMimeType(QMimeDatabase().mimeTypeForName(m_supportedMimeTypes.at(index))); } void CreateDialog::slotUpdateFilenameExtension(int index) { m_ui->chkAddExtension->setText(i18nc("the argument is a file extension (the period is not a typo)", "Automatically add .%1", QMimeDatabase().mimeTypeForName(m_supportedMimeTypes.at(index)).preferredSuffix())); } QUrl CreateDialog::selectedUrl() const { QString fileName = m_ui->filenameLineEdit->text(); QString dir = m_ui->destFolderUrlRequester->url().toLocalFile(); if (dir.trimmed().endsWith(QLatin1Char('/'))) { dir = dir.trimmed(); } if (m_ui->chkAddExtension->isChecked()) { QString detectedSuffix = QMimeDatabase().suffixForFileName(m_ui->filenameLineEdit->text().trimmed()); if (!currentMimeType().suffixes().contains(detectedSuffix)) { if (!fileName.endsWith(QLatin1Char('.'))) { fileName.append(QLatin1Char('.')); } fileName.append(currentMimeType().preferredSuffix()); } } if (!dir.endsWith(QLatin1Char('/'))) { dir.append(QLatin1Char('/')); } return QUrl::fromLocalFile(dir + fileName); } int CreateDialog::compressionLevel() const { return m_ui->optionsWidget->compressionLevel(); } QString CreateDialog::compressionMethod() const { return m_ui->optionsWidget->compressionMethod(); } QString CreateDialog::encryptionMethod() const { return m_ui->optionsWidget->encryptionMethod(); } ulong CreateDialog::volumeSize() const { return m_ui->optionsWidget->volumeSize(); } QString CreateDialog::password() const { return m_ui->optionsWidget->password(); } bool CreateDialog::isEncryptionAvailable() const { return m_ui->optionsWidget->isEncryptionAvailable(); } bool CreateDialog::isEncryptionEnabled() const { return m_ui->optionsWidget->isEncryptionEnabled(); } bool CreateDialog::isHeaderEncryptionAvailable() const { return m_ui->optionsWidget->isHeaderEncryptionAvailable(); } bool CreateDialog::isHeaderEncryptionEnabled() const { return m_ui->optionsWidget->isHeaderEncryptionEnabled(); } void CreateDialog::accept() { if (!isEncryptionEnabled()) { QDialog::accept(); return; } switch (m_ui->optionsWidget->passwordStatus()) { case KNewPasswordWidget::WeakPassword: case KNewPasswordWidget::StrongPassword: QDialog::accept(); break; case KNewPasswordWidget::PasswordNotVerified: KMessageBox::error(nullptr, i18n("The chosen password does not match the given verification password.")); break; default: break; } } void CreateDialog::slotUpdateDefaultMimeType() { m_config.writeEntry("LastMimeType", currentMimeType().name()); } void CreateDialog::loadConfiguration() { m_config = KConfigGroup(KSharedConfig::openConfig()->group("CreateDialog")); QMimeType lastUsedMime = QMimeDatabase().mimeTypeForName(m_config.readEntry("LastMimeType", QStringLiteral("application/x-compressed-tar"))); setMimeType(lastUsedMime.name()); } QMimeType CreateDialog::currentMimeType() const { Q_ASSERT(m_supportedMimeTypes.size() > m_ui->mimeComboBox->currentIndex()); return QMimeDatabase().mimeTypeForName(m_supportedMimeTypes.at(m_ui->mimeComboBox->currentIndex())); } bool CreateDialog::setMimeType(const QString &mimeTypeName) { int index = m_supportedMimeTypes.indexOf(mimeTypeName); if (index == -1) { return false; } m_ui->mimeComboBox->setCurrentIndex(index); // This is needed to make sure widgets get updated in case the mimetype is already selected. slotUpdateWidgets(index); return true; } } #include "createdialog.moc" diff --git a/kerfuffle/extractiondialog.cpp b/kerfuffle/extractiondialog.cpp index da00bb91..db5b3987 100644 --- a/kerfuffle/extractiondialog.cpp +++ b/kerfuffle/extractiondialog.cpp @@ -1,329 +1,327 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * * 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 "extractiondialog.h" #include "ark_debug.h" #include "settings.h" #include #include #include #include #include #include #include #include #include "ui_extractiondialog.h" namespace Kerfuffle { class ExtractionDialogUI: public QFrame, public Ui::ExtractionDialog { Q_OBJECT public: ExtractionDialogUI(QWidget *parent = nullptr) : QFrame(parent) { setupUi(this); } }; ExtractionDialog::ExtractionDialog(QWidget *parent) : QDialog(parent, Qt::Dialog) { - qCDebug(ARK) << "ExtractionDialog loaded"; - setWindowTitle(i18nc("@title:window", "Extract")); QHBoxLayout *hlayout = new QHBoxLayout(); setLayout(hlayout); fileWidget = new KFileWidget(QUrl::fromLocalFile(QDir::homePath()), this); hlayout->addWidget(fileWidget); fileWidget->setMode(KFile::Directory | KFile::LocalOnly | KFile::ExistingOnly); fileWidget->setOperationMode(KFileWidget::Saving); // This signal is emitted e.g. when the user presses Return while in the location bar. connect(fileWidget, &KFileWidget::accepted, this, &ExtractionDialog::slotAccepted); fileWidget->okButton()->setText(i18n("Extract")); fileWidget->okButton()->show(); connect(fileWidget->okButton(), &QPushButton::clicked, this, &ExtractionDialog::slotAccepted); fileWidget->cancelButton()->show(); connect(fileWidget->cancelButton(), &QPushButton::clicked, this, &QDialog::reject); m_ui = new ExtractionDialogUI(this); hlayout->addWidget(m_ui); m_ui->iconLabel->setPixmap(QIcon::fromTheme(QStringLiteral("archive-extract")).pixmap(IconSize(KIconLoader::Desktop), IconSize(KIconLoader::Desktop))); m_ui->filesToExtractGroupBox->hide(); m_ui->allFilesButton->setChecked(true); m_ui->extractAllLabel->show(); m_ui->autoSubfolders->hide(); loadSettings(); connect(this, &QDialog::finished, this, &ExtractionDialog::writeSettings); } void ExtractionDialog::slotAccepted() { // If an item is selected, enter it if it exists and is a dir. if (!fileWidget->dirOperator()->selectedItems().isEmpty()) { QFileInfo fi(fileWidget->dirOperator()->selectedItems().urlList().first().path()); if (fi.isDir() && fi.exists()) { fileWidget->locationEdit()->clear(); fileWidget->setUrl(QUrl::fromLocalFile(fi.absoluteFilePath())); } return; } // We extract to baseUrl(). const QString destinationPath = fileWidget->baseUrl().path(); // If extracting to a subfolder, we need to do some checks. if (extractToSubfolder()) { // Check if subfolder contains slashes. if (subfolder().contains(QLatin1String( "/" ))) { KMessageBox::error(this, i18n("The subfolder name may not contain the character '/'.")); return; } // Handle existing subfolder. const QString pathWithSubfolder = destinationPath + subfolder(); while (1) { if (QDir(pathWithSubfolder).exists()) { if (QFileInfo(pathWithSubfolder).isDir()) { int overwrite = KMessageBox::questionYesNoCancel(this, xi18nc("@info", "The folder %1 already exists. Are you sure you want to extract here?", pathWithSubfolder), i18n("Folder exists"), KGuiItem(i18n("Extract here")), KGuiItem(i18n("Retry"))); if (overwrite == KMessageBox::No) { // The user clicked Retry. continue; } else if (overwrite == KMessageBox::Cancel) { return; } } else { KMessageBox::detailedError(this, xi18nc("@info", "The folder %1 could not be created.", subfolder()), xi18nc("@info", "%1 already exists, but is not a folder.", subfolder())); return; } } else if (!QDir().mkdir(pathWithSubfolder)) { KMessageBox::detailedError(this, xi18nc("@info", "The folder %1 could not be created.", subfolder()), i18n("Please check your permissions to create it.")); return; } break; } } // Add new destination value to arkrc for quickExtractMenu. KConfigGroup conf(KSharedConfig::openConfig(), "ExtractDialog"); QStringList destHistory = conf.readPathEntry("DirHistory", QStringList()); destHistory.prepend(destinationPath); destHistory.removeDuplicates(); if (destHistory.size() > 10) { destHistory.removeLast(); } conf.writePathEntry ("DirHistory", destHistory); fileWidget->accept(); accept(); } void ExtractionDialog::loadSettings() { setOpenDestinationFolderAfterExtraction(ArkSettings::openDestinationFolderAfterExtraction()); setCloseAfterExtraction(ArkSettings::closeAfterExtraction()); setPreservePaths(ArkSettings::preservePaths()); } void ExtractionDialog::setExtractToSubfolder(bool extractToSubfolder) { m_ui->singleFolderGroup->setChecked(extractToSubfolder && ArkSettings::extractToSubfolder()); } void ExtractionDialog::batchModeOption() { m_ui->autoSubfolders->show(); m_ui->autoSubfolders->setEnabled(true); m_ui->singleFolderGroup->hide(); m_ui->extractAllLabel->setText(i18n("Extract multiple archives")); } void ExtractionDialog::setSubfolder(const QString& subfolder) { m_ui->subfolder->setText(subfolder); } QString ExtractionDialog::subfolder() const { return m_ui->subfolder->text(); } void ExtractionDialog::setBusyGui() { QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); fileWidget->setEnabled(false); m_ui->setEnabled(false); // TODO: tell the user why the dialog is busy (e.g. "archive is being loaded"). } void ExtractionDialog::setReadyGui() { QApplication::restoreOverrideCursor(); fileWidget->setEnabled(true); m_ui->setEnabled(true); } ExtractionDialog::~ExtractionDialog() { delete m_ui; m_ui = nullptr; } void ExtractionDialog::setShowSelectedFiles(bool value) { if (value) { m_ui->filesToExtractGroupBox->show(); m_ui->selectedFilesButton->setChecked(true); m_ui->extractAllLabel->hide(); } else { m_ui->filesToExtractGroupBox->hide(); m_ui->selectedFilesButton->setChecked(false); m_ui->extractAllLabel->show(); } } bool ExtractionDialog::extractAllFiles() const { return m_ui->allFilesButton->isChecked(); } void ExtractionDialog::setAutoSubfolder(bool value) { m_ui->autoSubfolders->setChecked(value); } bool ExtractionDialog::autoSubfolders() const { return m_ui->autoSubfolders->isChecked(); } bool ExtractionDialog::extractToSubfolder() const { return m_ui->singleFolderGroup->isChecked(); } void ExtractionDialog::setOpenDestinationFolderAfterExtraction(bool value) { m_ui->openFolderCheckBox->setChecked(value); } void ExtractionDialog::setCloseAfterExtraction(bool value) { m_ui->closeAfterExtraction->setChecked(value); } void ExtractionDialog::setPreservePaths(bool value) { m_ui->preservePaths->setChecked(value); } bool ExtractionDialog::preservePaths() const { return m_ui->preservePaths->isChecked(); } bool ExtractionDialog::openDestinationAfterExtraction() const { return m_ui->openFolderCheckBox->isChecked(); } bool ExtractionDialog::closeAfterExtraction() const { return m_ui->closeAfterExtraction->isChecked(); } QUrl ExtractionDialog::destinationDirectory() const { if (extractToSubfolder()) { QUrl subUrl = fileWidget->baseUrl(); if (subUrl.path().endsWith(QDir::separator())) { subUrl.setPath(subUrl.path() + subfolder()); } else { subUrl.setPath(subUrl.path() + QDir::separator() + subfolder()); } return subUrl; } else { return fileWidget->baseUrl(); } } void ExtractionDialog::writeSettings() { ArkSettings::setOpenDestinationFolderAfterExtraction(openDestinationAfterExtraction()); ArkSettings::setCloseAfterExtraction(closeAfterExtraction()); ArkSettings::setPreservePaths(preservePaths()); ArkSettings::self()->save(); // Save dialog window size KConfigGroup group(KSharedConfig::openConfig(), "ExtractDialog"); KWindowConfig::saveWindowSize(windowHandle(), group, KConfigBase::Persistent); } void ExtractionDialog::setCurrentUrl(const QUrl &url) { fileWidget->setUrl(url); } void ExtractionDialog::restoreWindowSize() { // Restore window size from config file, needs a windowHandle so must be called after show() KConfigGroup group(KSharedConfig::openConfig(), "ExtractDialog"); KWindowConfig::restoreWindowSize(windowHandle(), group); } } #include "extractiondialog.moc" diff --git a/kerfuffle/jobs.cpp b/kerfuffle/jobs.cpp index 2dfe18b4..db9e3211 100644 --- a/kerfuffle/jobs.cpp +++ b/kerfuffle/jobs.cpp @@ -1,854 +1,854 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "jobs.h" #include "archiveentry.h" #include "ark_debug.h" #include #include #include #include #include #include #include #include #include namespace Kerfuffle { class Job::Private : public QThread { Q_OBJECT public: Private(Job *job, QObject *parent = nullptr) : QThread(parent) , q(job) { } void run() override; private: Job *q; }; void Job::Private::run() { q->doWork(); } Job::Job(Archive *archive, ReadOnlyArchiveInterface *interface) : KJob() , m_archive(archive) , m_archiveInterface(interface) , d(new Private(this)) { setCapabilities(KJob::Killable); } Job::Job(Archive *archive) : Job(archive, nullptr) {} Job::Job(ReadOnlyArchiveInterface *interface) : Job(nullptr, interface) {} Job::~Job() { if (d->isRunning()) { d->wait(); } delete d; } ReadOnlyArchiveInterface *Job::archiveInterface() { // Use the archive interface. if (archive()) { return archive()->interface(); } // Use the interface passed to this job (e.g. JSONArchiveInterface in jobstest.cpp). return m_archiveInterface; } Archive *Job::archive() const { return m_archive; } QString Job::errorString() const { if (!errorText().isEmpty()) { return errorText(); } if (archive()) { if (archive()->error() == NoPlugin) { return i18n("No suitable plugin found. Ark does not seem to support this file type."); } if (archive()->error() == FailedPlugin) { return i18n("Failed to load a suitable plugin. Make sure any executables needed to handle the archive type are installed."); } } return QString(); } void Job::start() { jobTimer.start(); // We have an archive but it's not valid, nothing to do. if (archive() && !archive()->isValid()) { QTimer::singleShot(0, this, [=]() { onFinished(false); }); return; } if (archiveInterface()->waitForFinishedSignal()) { // CLI-based interfaces run a QProcess, no need to use threads. QTimer::singleShot(0, this, &Job::doWork); } else { // Run the job in another thread. d->start(); } } void Job::connectToArchiveInterfaceSignals() { connect(archiveInterface(), &ReadOnlyArchiveInterface::cancelled, this, &Job::onCancelled); connect(archiveInterface(), &ReadOnlyArchiveInterface::error, this, &Job::onError); connect(archiveInterface(), &ReadOnlyArchiveInterface::entry, this, &Job::onEntry); connect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &Job::onProgress); connect(archiveInterface(), &ReadOnlyArchiveInterface::info, this, &Job::onInfo); connect(archiveInterface(), &ReadOnlyArchiveInterface::finished, this, &Job::onFinished); connect(archiveInterface(), &ReadOnlyArchiveInterface::userQuery, this, &Job::onUserQuery); auto readWriteInterface = qobject_cast(archiveInterface()); if (readWriteInterface) { connect(readWriteInterface, &ReadWriteArchiveInterface::entryRemoved, this, &Job::onEntryRemoved); } } void Job::onCancelled() { qCDebug(ARK) << "Cancelled emitted"; setError(KJob::KilledJobError); } void Job::onError(const QString & message, const QString & details) { Q_UNUSED(details) qCDebug(ARK) << "Error emitted:" << message; setError(KJob::UserDefinedError); setErrorText(message); } void Job::onEntry(Archive::Entry *entry) { emit newEntry(entry); } void Job::onProgress(double value) { setPercent(static_cast(100.0*value)); } void Job::onInfo(const QString& info) { emit infoMessage(this, info); } void Job::onEntryRemoved(const QString & path) { emit entryRemoved(path); } void Job::onFinished(bool result) { qCDebug(ARK) << "Job finished, result:" << result << ", time:" << jobTimer.elapsed() << "ms"; if (archive() && !archive()->isValid()) { setError(KJob::UserDefinedError); } if (!d->isInterruptionRequested()) { emitResult(); } } void Job::onUserQuery(Query *query) { if (archiveInterface()->waitForFinishedSignal()) { qCWarning(ARK) << "Plugins run from the main thread should call directly query->execute()"; } emit userQuery(query); } bool Job::doKill() { const bool canKillJob = archiveInterface()->doKill(); if (!d->isRunning()) { return canKillJob; } d->requestInterruption(); if (!canKillJob) { qCDebug(ARK) << "Forcing thread exit in one second..."; d->wait(1000); return true; } d->wait(); return canKillJob; } LoadJob::LoadJob(Archive *archive, ReadOnlyArchiveInterface *interface) : Job(archive, interface) , m_isSingleFolderArchive(true) , m_isPasswordProtected(false) , m_extractedFilesSize(0) , m_dirCount(0) , m_filesCount(0) { - qCDebug(ARK) << "LoadJob created"; + qCDebug(ARK) << "Created job instance"; connect(this, &LoadJob::newEntry, this, &LoadJob::onNewEntry); } LoadJob::LoadJob(Archive *archive) : LoadJob(archive, nullptr) {} LoadJob::LoadJob(ReadOnlyArchiveInterface *interface) : LoadJob(nullptr, interface) {} void LoadJob::doWork() { emit description(this, i18n("Loading archive"), qMakePair(i18n("Archive"), archiveInterface()->filename())); connectToArchiveInterfaceSignals(); bool ret = archiveInterface()->list(); if (!archiveInterface()->waitForFinishedSignal()) { // onFinished() needs to be called after onNewEntry(), because the former reads members set in the latter. // So we need to put it in the event queue, just like the single-thread case does by emitting finished(). QTimer::singleShot(0, this, [=]() { onFinished(ret); }); } } void LoadJob::onFinished(bool result) { if (archive() && result) { archive()->setProperty("unpackedSize", extractedFilesSize()); archive()->setProperty("isSingleFolder", isSingleFolderArchive()); const auto name = subfolderName().isEmpty() ? archive()->completeBaseName() : subfolderName(); archive()->setProperty("subfolderName", name); if (isPasswordProtected()) { archive()->setProperty("encryptionType", archive()->password().isEmpty() ? Archive::Encrypted : Archive::HeaderEncrypted); } } Job::onFinished(result); } qlonglong LoadJob::extractedFilesSize() const { return m_extractedFilesSize; } bool LoadJob::isPasswordProtected() const { return m_isPasswordProtected; } bool LoadJob::isSingleFolderArchive() const { if (m_filesCount == 1 && m_dirCount == 0) { return false; } return m_isSingleFolderArchive; } void LoadJob::onNewEntry(const Archive::Entry *entry) { m_extractedFilesSize += entry->property("size").toLongLong(); m_isPasswordProtected |= entry->property("isPasswordProtected").toBool(); if (entry->isDir()) { m_dirCount++; } else { m_filesCount++; } if (m_isSingleFolderArchive) { // RPM filenames have the ./ prefix, and "." would be detected as the subfolder name, so we remove it. const QString fullPath = entry->fullPath().replace(QRegularExpression(QStringLiteral("^\\./")), QString()); const QString basePath = fullPath.split(QLatin1Char('/')).at(0); if (m_basePath.isEmpty()) { m_basePath = basePath; m_subfolderName = basePath; } else { if (m_basePath != basePath) { m_isSingleFolderArchive = false; m_subfolderName.clear(); } } } } QString LoadJob::subfolderName() const { if (!isSingleFolderArchive()) { return QString(); } return m_subfolderName; } BatchExtractJob::BatchExtractJob(LoadJob *loadJob, const QString &destination, bool autoSubfolder, bool preservePaths) : Job(loadJob->archive()) , m_loadJob(loadJob) , m_destination(destination) , m_autoSubfolder(autoSubfolder) , m_preservePaths(preservePaths) { - qCDebug(ARK) << "BatchExtractJob created"; + qCDebug(ARK) << "Created job instance"; } void BatchExtractJob::doWork() { connect(m_loadJob, &KJob::result, this, &BatchExtractJob::slotLoadingFinished); connect(archiveInterface(), &ReadOnlyArchiveInterface::cancelled, this, &BatchExtractJob::onCancelled); if (archiveInterface()->hasBatchExtractionProgress()) { // progress() will be actually emitted by the LoadJob, but the archiveInterface() is the same. connect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &BatchExtractJob::slotLoadingProgress); } // Forward LoadJob's signals. connect(m_loadJob, &Kerfuffle::Job::newEntry, this, &BatchExtractJob::newEntry); connect(m_loadJob, &Kerfuffle::Job::userQuery, this, &BatchExtractJob::userQuery); m_loadJob->start(); } bool BatchExtractJob::doKill() { if (m_step == Loading) { return m_loadJob->kill(); } return m_extractJob->kill(); } void BatchExtractJob::slotLoadingProgress(double progress) { // Progress from LoadJob counts only for 50% of the BatchExtractJob's duration. m_lastPercentage = static_cast(50.0*progress); setPercent(m_lastPercentage); } void BatchExtractJob::slotExtractProgress(double progress) { // The 2nd 50% of the BatchExtractJob's duration comes from the ExtractJob. setPercent(m_lastPercentage + static_cast(50.0*progress)); } void BatchExtractJob::slotLoadingFinished(KJob *job) { if (job->error()) { // Forward errors as well. onError(job->errorString(), QString()); onFinished(false); return; } // Now we can start extraction. setupDestination(); Kerfuffle::ExtractionOptions options; options.setPreservePaths(m_preservePaths); m_extractJob = archive()->extractFiles({}, m_destination, options); if (m_extractJob) { connect(m_extractJob, &KJob::result, this, &BatchExtractJob::emitResult); connect(m_extractJob, &Kerfuffle::Job::userQuery, this, &BatchExtractJob::userQuery); if (archiveInterface()->hasBatchExtractionProgress()) { // The LoadJob is done, change slot and start setting the percentage from m_lastPercentage on. disconnect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &BatchExtractJob::slotLoadingProgress); connect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &BatchExtractJob::slotExtractProgress); } m_step = Extracting; m_extractJob->start(); } else { emitResult(); } } void BatchExtractJob::setupDestination() { const bool isSingleFolderRPM = (archive()->isSingleFolder() && (archive()->mimeType().name() == QLatin1String("application/x-rpm"))); if (m_autoSubfolder && (!archive()->isSingleFolder() || isSingleFolderRPM)) { const QDir d(m_destination); QString subfolderName = archive()->subfolderName(); // Special case for single folder RPM archives. // We don't want the autodetected folder to have a meaningless "usr" name. if (isSingleFolderRPM && subfolderName == QStringLiteral("usr")) { qCDebug(ARK) << "Detected single folder RPM archive. Using archive basename as subfolder name"; subfolderName = QFileInfo(archive()->fileName()).completeBaseName(); } if (d.exists(subfolderName)) { subfolderName = KIO::suggestName(QUrl::fromUserInput(m_destination, QString(), QUrl::AssumeLocalFile), subfolderName); } d.mkdir(subfolderName); m_destination += QLatin1Char( '/' ) + subfolderName; } } CreateJob::CreateJob(Archive *archive, const QVector &entries, const CompressionOptions &options) : Job(archive) , m_entries(entries) , m_options(options) { - qCDebug(ARK) << "CreateJob created"; + qCDebug(ARK) << "Created job instance"; } void CreateJob::enableEncryption(const QString &password, bool encryptHeader) { archive()->encrypt(password, encryptHeader); } void CreateJob::setMultiVolume(bool isMultiVolume) { archive()->setMultiVolume(isMultiVolume); } void CreateJob::doWork() { m_addJob = archive()->addFiles(m_entries, nullptr, m_options); if (m_addJob) { connect(m_addJob, &KJob::result, this, &CreateJob::emitResult); // Forward description signal from AddJob, we need to change the first argument ('this' needs to be a CreateJob). connect(m_addJob, &KJob::description, this, [=](KJob *, const QString &title, const QPair &field1, const QPair &) { emit description(this, title, field1); }); m_addJob->start(); } else { emitResult(); } } bool CreateJob::doKill() { return m_addJob && m_addJob->kill(); } ExtractJob::ExtractJob(const QVector &entries, const QString &destinationDir, const ExtractionOptions &options, ReadOnlyArchiveInterface *interface) : Job(interface) , m_entries(entries) , m_destinationDir(destinationDir) , m_options(options) { - qCDebug(ARK) << "ExtractJob created"; + qCDebug(ARK) << "Created job instance"; } void ExtractJob::doWork() { QString desc; if (m_entries.count() == 0) { desc = i18n("Extracting all files"); } else { desc = i18np("Extracting one file", "Extracting %1 files", m_entries.count()); } emit description(this, desc, qMakePair(i18n("Archive"), archiveInterface()->filename()), qMakePair(i18nc("extraction folder", "Destination"), m_destinationDir)); QFileInfo destDirInfo(m_destinationDir); if (destDirInfo.isDir() && (!destDirInfo.isWritable() || !destDirInfo.isExecutable())) { onError(xi18n("Could not write to destination %1.Check whether you have sufficient permissions.", m_destinationDir), QString()); onFinished(false); return; } connectToArchiveInterfaceSignals(); qCDebug(ARK) << "Starting extraction with" << m_entries.count() << "selected files." << m_entries << "Destination dir:" << m_destinationDir << "Options:" << m_options; bool ret = archiveInterface()->extractFiles(m_entries, m_destinationDir, m_options); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } QString ExtractJob::destinationDirectory() const { return m_destinationDir; } ExtractionOptions ExtractJob::extractionOptions() const { return m_options; } TempExtractJob::TempExtractJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : Job(interface) , m_entry(entry) , m_passwordProtectedHint(passwordProtectedHint) { m_tmpExtractDir = new QTemporaryDir(); } QString TempExtractJob::validatedFilePath() const { QString path = extractionDir() + QLatin1Char('/') + m_entry->fullPath(); // Make sure a maliciously crafted archive with parent folders named ".." do // not cause the previewed file path to be located outside the temporary // directory, resulting in a directory traversal issue. path.remove(QStringLiteral("../")); return path; } ExtractionOptions TempExtractJob::extractionOptions() const { ExtractionOptions options; if (m_passwordProtectedHint) { options.setEncryptedArchiveHint(true); } return options; } QTemporaryDir *TempExtractJob::tempDir() const { return m_tmpExtractDir; } void TempExtractJob::doWork() { // pass 1 to i18np on purpose so this translation may properly be reused. emit description(this, i18np("Extracting one file", "Extracting %1 files", 1)); connectToArchiveInterfaceSignals(); qCDebug(ARK) << "Extracting:" << m_entry; bool ret = archiveInterface()->extractFiles({m_entry}, extractionDir(), extractionOptions()); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } QString TempExtractJob::extractionDir() const { return m_tmpExtractDir->path(); } PreviewJob::PreviewJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : TempExtractJob(entry, passwordProtectedHint, interface) { - qCDebug(ARK) << "PreviewJob created"; + qCDebug(ARK) << "Created job instance"; } OpenJob::OpenJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : TempExtractJob(entry, passwordProtectedHint, interface) { - qCDebug(ARK) << "OpenJob created"; + qCDebug(ARK) << "Created job instance"; } OpenWithJob::OpenWithJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : OpenJob(entry, passwordProtectedHint, interface) { - qCDebug(ARK) << "OpenWithJob created"; + qCDebug(ARK) << "Created job instance"; } AddJob::AddJob(const QVector &entries, const Archive::Entry *destination, const CompressionOptions& options, ReadWriteArchiveInterface *interface) : Job(interface) , m_entries(entries) , m_destination(destination) , m_options(options) { - qCDebug(ARK) << "AddJob created"; + qCDebug(ARK) << "Created job instance"; } void AddJob::doWork() { // Set current dir. const QString globalWorkDir = m_options.globalWorkDir(); const QDir workDir = globalWorkDir.isEmpty() ? QDir::current() : QDir(globalWorkDir); if (!globalWorkDir.isEmpty()) { qCDebug(ARK) << "GlobalWorkDir is set, changing dir to " << globalWorkDir; m_oldWorkingDir = QDir::currentPath(); QDir::setCurrent(globalWorkDir); } // Count total number of entries to be added. uint totalCount = 0; QElapsedTimer timer; timer.start(); foreach (const Archive::Entry* entry, m_entries) { totalCount++; if (QFileInfo(entry->fullPath()).isDir()) { QDirIterator it(entry->fullPath(), QDir::AllEntries | QDir::Readable | QDir::Hidden | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (it.hasNext()) { it.next(); totalCount++; } } } - qCDebug(ARK) << "AddJob: going to add" << totalCount << "entries, counted in" << timer.elapsed() << "ms"; + qCDebug(ARK) << "Going to add" << totalCount << "entries, counted in" << timer.elapsed() << "ms"; const QString desc = i18np("Compressing a file", "Compressing %1 files", totalCount); emit description(this, desc, qMakePair(i18n("Archive"), archiveInterface()->filename())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); // The file paths must be relative to GlobalWorkDir. foreach (Archive::Entry *entry, m_entries) { // #191821: workDir must be used instead of QDir::current() // so that symlinks aren't resolved automatically const QString &fullPath = entry->fullPath(); QString relativePath = workDir.relativeFilePath(fullPath); if (fullPath.endsWith(QLatin1Char('/'))) { relativePath += QLatin1Char('/'); } entry->setFullPath(relativePath); } connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->addFiles(m_entries, m_destination, m_options, totalCount); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void AddJob::onFinished(bool result) { if (!m_oldWorkingDir.isEmpty()) { QDir::setCurrent(m_oldWorkingDir); } Job::onFinished(result); } MoveJob::MoveJob(const QVector &entries, Archive::Entry *destination, const CompressionOptions& options , ReadWriteArchiveInterface *interface) : Job(interface) , m_finishedSignalsCount(0) , m_entries(entries) , m_destination(destination) , m_options(options) { - qCDebug(ARK) << "MoveJob created"; + qCDebug(ARK) << "Created job instance"; } void MoveJob::doWork() { - qCDebug(ARK) << "MoveJob: going to move" << m_entries.count() << "file(s)"; + qCDebug(ARK) << "Going to move" << m_entries.count() << "file(s)"; QString desc = i18np("Moving a file", "Moving %1 files", m_entries.count()); emit description(this, desc, qMakePair(i18n("Archive"), archiveInterface()->filename())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->moveFiles(m_entries, m_destination, m_options); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void MoveJob::onFinished(bool result) { m_finishedSignalsCount++; if (m_finishedSignalsCount == archiveInterface()->moveRequiredSignals()) { Job::onFinished(result); } } CopyJob::CopyJob(const QVector &entries, Archive::Entry *destination, const CompressionOptions &options, ReadWriteArchiveInterface *interface) : Job(interface) , m_finishedSignalsCount(0) , m_entries(entries) , m_destination(destination) , m_options(options) { - qCDebug(ARK) << "CopyJob created"; + qCDebug(ARK) << "Created job instance"; } void CopyJob::doWork() { - qCDebug(ARK) << "CopyJob: going to copy" << m_entries.count() << "file(s)"; + qCDebug(ARK) << "Going to copy" << m_entries.count() << "file(s)"; QString desc = i18np("Copying a file", "Copying %1 files", m_entries.count()); emit description(this, desc, qMakePair(i18n("Archive"), archiveInterface()->filename())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->copyFiles(m_entries, m_destination, m_options); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void CopyJob::onFinished(bool result) { m_finishedSignalsCount++; if (m_finishedSignalsCount == archiveInterface()->copyRequiredSignals()) { Job::onFinished(result); } } DeleteJob::DeleteJob(const QVector &entries, ReadWriteArchiveInterface *interface) : Job(interface) , m_entries(entries) { } void DeleteJob::doWork() { QString desc = i18np("Deleting a file from the archive", "Deleting %1 files", m_entries.count()); emit description(this, desc, qMakePair(i18n("Archive"), archiveInterface()->filename())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->deleteFiles(m_entries); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } CommentJob::CommentJob(const QString& comment, ReadWriteArchiveInterface *interface) : Job(interface) , m_comment(comment) { } void CommentJob::doWork() { emit description(this, i18n("Adding comment")); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->addComment(m_comment); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } TestJob::TestJob(ReadOnlyArchiveInterface *interface) : Job(interface) { m_testSuccess = false; } void TestJob::doWork() { - qCDebug(ARK) << "TestJob started"; + qCDebug(ARK) << "Job started"; emit description(this, i18n("Testing archive"), qMakePair(i18n("Archive"), archiveInterface()->filename())); connectToArchiveInterfaceSignals(); connect(archiveInterface(), &ReadOnlyArchiveInterface::testSuccess, this, &TestJob::onTestSuccess); bool ret = archiveInterface()->testArchive(); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void TestJob::onTestSuccess() { m_testSuccess = true; } bool TestJob::testSucceeded() { return m_testSuccess; } } // namespace Kerfuffle #include "jobs.moc" diff --git a/kerfuffle/propertiesdialog.cpp b/kerfuffle/propertiesdialog.cpp index 6b58f046..64dba9a6 100644 --- a/kerfuffle/propertiesdialog.cpp +++ b/kerfuffle/propertiesdialog.cpp @@ -1,147 +1,145 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2016 Ragnar Thomsen * * 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 "propertiesdialog.h" #include "archive_kerfuffle.h" #include "ark_debug.h" #include "ui_propertiesdialog.h" #include #include #include #include #include #include #include #include namespace Kerfuffle { class PropertiesDialogUI: public QWidget, public Ui::PropertiesDialog { Q_OBJECT public: PropertiesDialogUI(QWidget *parent = nullptr) : QWidget(parent) { setupUi(this); } }; PropertiesDialog::PropertiesDialog(QWidget *parent, Archive *archive, qulonglong numberOfFiles, qulonglong numberOfFolders, qulonglong size) : QDialog(parent, Qt::Dialog) { - qCDebug(ARK) << "PropertiesDialog loaded"; - QFileInfo fi(archive->fileName()); setWindowTitle(i18nc("@title:window", "Properties for %1", fi.fileName())); setModal(true); m_ui = new PropertiesDialogUI(this); m_ui->lblArchiveName->setText(archive->fileName()); m_ui->lblArchiveType->setText(archive->mimeType().comment()); m_ui->lblMimetype->setText(archive->mimeType().name()); m_ui->lblCompressionMethods->setText(archive->property("compressionMethods").toStringList().join(QStringLiteral(", "))); m_ui->lblReadOnly->setText(archive->isReadOnly() ? i18n("yes") : i18n("no")); m_ui->lblMultiVolume->setText(archive->isMultiVolume() ? i18n("yes (%1 volumes)", archive->numberOfVolumes()) : i18n("no")); m_ui->lblHasComment->setText(archive->hasComment() ? i18n("yes") : i18n("no")); m_ui->lblNumberOfEntries->setText(i18np("%1 file", "%1 files", numberOfFiles) + i18np(", %1 folder", ", %1 folders", numberOfFolders)); m_ui->lblUnpackedSize->setText(KIO::convertSize(size)); m_ui->lblPackedSize->setText(KIO::convertSize(archive->packedSize())); m_ui->lblCompressionRatio->setText(QString::number(float(archive->unpackedSize()) / float(archive->packedSize()), 'f', 1)); m_ui->lblLastModified->setText(fi.lastModified().toString(QStringLiteral("yyyy-MM-dd HH:mm"))); m_ui->lblMD5->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); m_ui->lblSHA1->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); m_ui->lblSHA256->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); switch (archive->encryptionType()) { case Archive::Unencrypted: m_ui->lblPasswordProtected->setText(i18n("no")); break; case Archive::Encrypted: m_ui->lblPasswordProtected->setText(i18n("only file contents (%1)", archive->property("encryptionMethods").toStringList().join(QStringLiteral(", ")))); break; case Archive::HeaderEncrypted: m_ui->lblPasswordProtected->setText(i18n("yes (%1)", archive->property("encryptionMethods").toStringList().join(QStringLiteral(", ")))); break; } // The Sha256 label is populated with 64 chars in the ui file. We fix the // size of the label so the dialog won't resize when the hashes are // calculated. This is an ugly hack and requires e.g. that we use monospace // font for the hashes. m_ui->lblSHA256->setMinimumSize(m_ui->lblSHA256->sizeHint()); m_ui->adjustSize(); setFixedSize(m_ui->size()); // Show an icon representing the mimetype of the archive. QIcon icon = QIcon::fromTheme(archive->mimeType().iconName()); m_ui->lblIcon->setPixmap(icon.pixmap(IconSize(KIconLoader::Desktop), IconSize(KIconLoader::Desktop))); m_ui->lblMD5->setText(i18n("Calculating...")); m_ui->lblSHA1->setText(i18n("Calculating...")); m_ui->lblSHA256->setText(i18n("Calculating...")); showChecksum(QCryptographicHash::Md5, archive->fileName(), m_ui->lblMD5); showChecksum(QCryptographicHash::Sha1, archive->fileName(), m_ui->lblSHA1); showChecksum(QCryptographicHash::Sha256, archive->fileName(), m_ui->lblSHA256); connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); } QString PropertiesDialog::calcHash(QCryptographicHash::Algorithm algorithm, const QString &path) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) { return QString(); } QCryptographicHash hash(algorithm); hash.addData(&file); return QString::fromLatin1(hash.result().toHex()); } void PropertiesDialog::showChecksum(QCryptographicHash::Algorithm algorithm, const QString &fileName, QLabel *label) { // Calculate checksum in another thread. auto futureWatcher = new QFutureWatcher(this); connect(futureWatcher, &QFutureWatcher::finished, this, [=]() { label->setText(futureWatcher->result()); futureWatcher->deleteLater(); }); auto future = QtConcurrent::run(this, &PropertiesDialog::calcHash, algorithm, fileName); futureWatcher->setFuture(future); } #include "propertiesdialog.moc" } diff --git a/kerfuffle/queries.cpp b/kerfuffle/queries.cpp index 1d57d43f..3c018267 100644 --- a/kerfuffle/queries.cpp +++ b/kerfuffle/queries.cpp @@ -1,262 +1,262 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2008-2009 Harald Hvaal * * 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 "queries.h" #include "ark_debug.h" #include #include #include #include #include #include #include #include #include #include namespace Kerfuffle { Query::Query() { } QVariant Query::response() const { return m_data.value(QStringLiteral( "response" )); } void Query::waitForResponse() { QMutexLocker locker(&m_responseMutex); //if there is no response set yet, wait if (!m_data.contains(QStringLiteral("response"))) { m_responseCondition.wait(&m_responseMutex); } } void Query::setResponse(const QVariant &response) { m_data[QStringLiteral( "response" )] = response; m_responseCondition.wakeAll(); } OverwriteQuery::OverwriteQuery(const QString &filename) : m_noRenameMode(false), m_multiMode(true) { m_data[QStringLiteral("filename")] = filename; } void OverwriteQuery::execute() { // If we are being called from the KPart, the cursor is probably Qt::WaitCursor // at the moment (#231974) QApplication::setOverrideCursor(QCursor(Qt::ArrowCursor)); KIO::RenameDialog_Options options = KIO::RenameDialog_Overwrite | KIO::RenameDialog_Skip; if (m_noRenameMode) { options = options | KIO::RenameDialog_NoRename; } if (m_multiMode) { options = options | KIO::RenameDialog_MultipleItems; } QUrl sourceUrl = QUrl::fromLocalFile(QDir::cleanPath(m_data.value(QStringLiteral("filename")).toString())); QUrl destUrl = QUrl::fromLocalFile(QDir::cleanPath(m_data.value(QStringLiteral("filename")).toString())); QPointer dialog = new KIO::RenameDialog( nullptr, i18nc("@title:window", "File Already Exists"), sourceUrl, destUrl, options); dialog.data()->exec(); m_data[QStringLiteral("newFilename")] = dialog.data()->newDestUrl().toDisplayString(QUrl::PreferLocalFile); setResponse(dialog.data()->result()); delete dialog.data(); QApplication::restoreOverrideCursor(); } bool OverwriteQuery::responseCancelled() { return m_data.value(QStringLiteral( "response" )).toInt() == KIO::R_CANCEL; } bool OverwriteQuery::responseOverwriteAll() { return m_data.value(QStringLiteral( "response" )).toInt() == KIO::R_OVERWRITE_ALL; } bool OverwriteQuery::responseOverwrite() { return m_data.value(QStringLiteral( "response" )).toInt() == KIO::R_OVERWRITE; } bool OverwriteQuery::responseRename() { return m_data.value(QStringLiteral( "response" )).toInt() == KIO::R_RENAME; } bool OverwriteQuery::responseSkip() { return m_data.value(QStringLiteral( "response" )).toInt() == KIO::R_SKIP; } bool OverwriteQuery::responseAutoSkip() { return m_data.value(QStringLiteral( "response" )).toInt() == KIO::R_AUTO_SKIP; } QString OverwriteQuery::newFilename() { return m_data.value(QStringLiteral( "newFilename" )).toString(); } void OverwriteQuery::setNoRenameMode(bool enableNoRenameMode) { m_noRenameMode = enableNoRenameMode; } bool OverwriteQuery::noRenameMode() { return m_noRenameMode; } void OverwriteQuery::setMultiMode(bool enableMultiMode) { m_multiMode = enableMultiMode; } bool OverwriteQuery::multiMode() { return m_multiMode; } PasswordNeededQuery::PasswordNeededQuery(const QString& archiveFilename, bool incorrectTryAgain) { m_data[QStringLiteral( "archiveFilename" )] = archiveFilename; m_data[QStringLiteral( "incorrectTryAgain" )] = incorrectTryAgain; } void PasswordNeededQuery::execute() { qCDebug(ARK) << "Executing password prompt"; // If we are being called from the KPart, the cursor is probably Qt::WaitCursor // at the moment (#231974) QApplication::setOverrideCursor(QCursor(Qt::ArrowCursor)); QPointer dlg = new KPasswordDialog; dlg.data()->setPrompt(xi18nc("@info", "The archive %1 is password protected. Please enter the password.", m_data.value(QStringLiteral("archiveFilename")).toString())); if (m_data.value(QStringLiteral("incorrectTryAgain")).toBool()) { dlg.data()->showErrorMessage(i18n("Incorrect password, please try again."), KPasswordDialog::PasswordError); } const bool notCancelled = dlg.data()->exec(); const QString password = dlg.data()->password(); m_data[QStringLiteral("password")] = password; setResponse(notCancelled && !password.isEmpty()); QApplication::restoreOverrideCursor(); delete dlg.data(); } QString PasswordNeededQuery::password() { return m_data.value(QStringLiteral( "password" )).toString(); } bool PasswordNeededQuery::responseCancelled() { return !m_data.value(QStringLiteral( "response" )).toBool(); } LoadCorruptQuery::LoadCorruptQuery(const QString& archiveFilename) { m_data[QStringLiteral("archiveFilename")] = archiveFilename; } void LoadCorruptQuery::execute() { - qCDebug(ARK) << "Executing LoadCorrupt prompt"; + qCDebug(ARK) << "Executing prompt"; QApplication::setOverrideCursor(QCursor(Qt::ArrowCursor)); setResponse(KMessageBox::warningYesNo(nullptr, xi18nc("@info", "The archive you're trying to open is corrupt." "Some files may be missing or damaged."), i18nc("@title:window", "Corrupt archive"), KGuiItem(i18nc("@action:button", "Open as Read-Only")), KGuiItem(i18nc("@action:button", "Don't Open")))); QApplication::restoreOverrideCursor(); } bool LoadCorruptQuery::responseYes() { return (m_data.value(QStringLiteral("response")).toInt() == KMessageBox::Yes); } ContinueExtractionQuery::ContinueExtractionQuery(const QString& error, const QString& archiveEntry) : m_chkDontAskAgain(i18n("Don't ask again.")) { m_data[QStringLiteral("error")] = error; m_data[QStringLiteral("archiveEntry")] = archiveEntry; } void ContinueExtractionQuery::execute() { - qCDebug(ARK) << "Executing ContinueExtraction prompt"; + qCDebug(ARK) << "Executing prompt"; QApplication::setOverrideCursor(QCursor(Qt::ArrowCursor)); QMessageBox box(QMessageBox::Warning, i18n("Error during extraction"), xi18n("Extraction of the entry:" " %1" "failed with the error message: %2" "Do you want to continue extraction?", m_data.value(QStringLiteral("archiveEntry")).toString(), m_data.value(QStringLiteral("error")).toString()), QMessageBox::Yes|QMessageBox::Cancel); box.setCheckBox(&m_chkDontAskAgain); setResponse(box.exec()); QApplication::restoreOverrideCursor(); } bool ContinueExtractionQuery::responseCancelled() { return (m_data.value(QStringLiteral("response")).toInt() == QMessageBox::Cancel); } bool ContinueExtractionQuery::dontAskAgain() { return m_chkDontAskAgain.isChecked(); } } diff --git a/part/archiveview.cpp b/part/archiveview.cpp index 13c3f108..8c6f7d63 100644 --- a/part/archiveview.cpp +++ b/part/archiveview.cpp @@ -1,180 +1,180 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2008-2009 Harald Hvaal * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "archiveview.h" #include "ark_debug.h" #include #include #include #include #include #include #include ArchiveView::ArchiveView(QWidget *parent) : QTreeView(parent) { setSelectionMode(QAbstractItemView::ExtendedSelection); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); setAlternatingRowColors(true); setAnimated(true); setAllColumnsShowFocus(true); setSortingEnabled(true); setDragEnabled(true); setDropIndicatorShown(true); // #368807: drops must be initially disabled, otherwise they will override the MainWindow's ones. // They will be enabled in Part::slotLoadingFinished(). setDropsEnabled(false); header()->setSectionResizeMode(QHeaderView::ResizeToContents); } void ArchiveView::startDrag(Qt::DropActions supportedActions) { //only start the drag if it's over the filename column. this allows dragging selection in //tree/detail view if (currentIndex().column() != 0) { return; } QTreeView::startDrag(supportedActions); } void ArchiveView::expandIfSingleFolder() { if (model()->rowCount() == 1) { expandToDepth(0); } } void ArchiveView::setDropsEnabled(bool enabled) { setAcceptDrops(enabled); setDragDropMode(enabled ? QAbstractItemView::DragDrop : QAbstractItemView::DragOnly); } void ArchiveView::dragEnterEvent(QDragEnterEvent * event) { //TODO: if no model, trigger some mechanism to create one automatically! - qCDebug(ARK) << "dragEnterEvent" << event; + qCDebug(ARK) << event; if (event->source() == this) { //we don't support internal drops yet. return; } QTreeView::dragEnterEvent(event); } void ArchiveView::dropEvent(QDropEvent * event) { - qCDebug(ARK) << "dropEvent" << event; + qCDebug(ARK) << event; if (event->source() == this) { //we don't support internal drops yet. return; } QTreeView::dropEvent(event); } void ArchiveView::dragMoveEvent(QDragMoveEvent * event) { - qCDebug(ARK) << "dragMoveEvent" << event; + qCDebug(ARK) << event; if (event->source() == this) { //we don't support internal drops yet. return; } QTreeView::dragMoveEvent(event); if (event->mimeData()->hasFormat(QStringLiteral("text/uri-list"))) { event->acceptProposedAction(); } } bool ArchiveView::eventFilter(QObject *object, QEvent *event) { if (object == m_entryEditor && event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast(event); if (keyEvent->key() == Qt::Key_Escape) { closeEntryEditor(); return true; } } return false; } void ArchiveView::mouseReleaseEvent(QMouseEvent *event) { if (m_editorIndex.isValid()) { closeEntryEditor(); } else { QTreeView::mouseReleaseEvent(event); } } void ArchiveView::keyPressEvent(QKeyEvent *event) { if (m_editorIndex.isValid()) { switch (event->key()) { case Qt::Key_Return: case Qt::Key_Enter: { QLineEdit* editor = static_cast(indexWidget(m_editorIndex)); emit entryChanged(editor->text()); closeEntryEditor(); break; } default: QTreeView::keyPressEvent(event); } } else { QTreeView::keyPressEvent(event); } } void ArchiveView::renameSelectedEntry() { QModelIndex currentIndex = selectionModel()->currentIndex(); currentIndex = (currentIndex.parent().isValid()) ? currentIndex.parent().child(currentIndex.row(), 0) : model()->index(currentIndex.row(), 0); openEntryEditor(currentIndex); } void ArchiveView::openEntryEditor(const QModelIndex &index) { m_editorIndex = index; openPersistentEditor(index); m_entryEditor = static_cast(indexWidget(m_editorIndex)); m_entryEditor->installEventFilter(this); m_entryEditor->setText(index.data().toString()); m_entryEditor->setFocus(Qt::OtherFocusReason); m_entryEditor->selectAll(); } void ArchiveView::closeEntryEditor() { m_entryEditor->removeEventFilter(this); closePersistentEditor(m_editorIndex); m_editorIndex = QModelIndex(); } diff --git a/part/arkviewer.cpp b/part/arkviewer.cpp index d999dffb..d8c6362c 100644 --- a/part/arkviewer.cpp +++ b/part/arkviewer.cpp @@ -1,245 +1,243 @@ /* * ark: A program for modifying archives via a GUI. * * Copyright (C) 2004-2008 Henrique Pinto * * 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 "arkviewer.h" #include "ark_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include ArkViewer::ArkViewer() : KParts::MainWindow() { - qCDebug(ARK) << "ArkViewer opened"; - setupUi(this); m_buttonBox->button(QDialogButtonBox::Close)->setShortcut(Qt::Key_Escape); // Bug 369390: This prevents the Enter key from closing the window. m_buttonBox->button(QDialogButtonBox::Close)->setAutoDefault(false); connect(m_buttonBox, &QDialogButtonBox::rejected, this, &QMainWindow::close); setXMLFile(QStringLiteral("ark_viewer.rc")); setupGUI(ToolBar); } ArkViewer::~ArkViewer() { if (m_part) { QProgressDialog progressDialog(this); progressDialog.setWindowTitle(i18n("Closing preview")); progressDialog.setLabelText(i18n("Please wait while the preview is being closed...")); progressDialog.setMinimumDuration(500); progressDialog.setModal(true); progressDialog.setCancelButton(nullptr); progressDialog.setRange(0, 0); // #261785: this preview dialog is not modal, so we need to delete // the previewed file ourselves when the dialog is closed; const QString previewedFilePath(m_part.data()->url().toDisplayString(QUrl::PreferLocalFile)); m_part.data()->closeUrl(); if (!previewedFilePath.isEmpty()) { QFile::remove(previewedFilePath); } } guiFactory()->removeClient(m_part); delete m_part; } void ArkViewer::view(const QString& fileName) { QMimeDatabase db; QMimeType mimeType = db.mimeTypeForFile(fileName); qCDebug(ARK) << "viewing" << fileName << "with mime type:" << mimeType.name(); KService::Ptr viewer = ArkViewer::getViewer(mimeType.name()); const bool needsExternalViewer = (viewer && !viewer->hasServiceType(QStringLiteral("KParts/ReadOnlyPart"))); if (needsExternalViewer) { // We have already resolved the MIME type and the service above. // So there is no point in using KRun::runUrl() which would need // to do the same again. qCDebug(ARK) << "Using external viewer"; const QList fileUrlList = {QUrl::fromLocalFile(fileName)}; // The last argument (tempFiles) set to true means that the temporary // file will be removed when the viewer application exits. KRun::runService(*viewer, fileUrlList, nullptr, true); return; } qCDebug(ARK) << "Attempting to use internal viewer"; bool viewInInternalViewer = true; if (!viewer) { // No internal viewer available for the file. Ask the user if it // should be previewed as text/plain. qCDebug(ARK) << "Internal viewer not available"; int response; if (!mimeType.isDefault()) { // File has a defined MIME type, and not the default // application/octet-stream. So it could be viewable as // plain text, ask the user. response = KMessageBox::warningContinueCancel(nullptr, xi18n("The internal viewer cannot preview this type of file(%1).Do you want to try to view it as plain text?", mimeType.name()), i18nc("@title:window", "Cannot Preview File"), KGuiItem(i18nc("@action:button", "Preview as Text"), QIcon::fromTheme(QStringLiteral("text-plain"))), KStandardGuiItem::cancel(), QStringLiteral("PreviewAsText_%1").arg(mimeType.name())); } else { // No defined MIME type, or the default application/octet-stream. // There is still a possibility that it could be viewable as plain // text, so ask the user. Not the same as the message/question // above, because the wording and default are different. response = KMessageBox::warningContinueCancel(nullptr, xi18n("The internal viewer cannot preview this unknown type of file.Do you want to try to view it as plain text?"), i18nc("@title:window", "Cannot Preview File"), KGuiItem(i18nc("@action:button", "Preview as Text"), QIcon::fromTheme(QStringLiteral("text-plain"))), KStandardGuiItem::cancel(), QString(), KMessageBox::Dangerous); } if (response == KMessageBox::Cancel) { viewInInternalViewer = false; } else { // set for viewer later mimeType = db.mimeTypeForName(QStringLiteral("text/plain")); } } if (viewInInternalViewer) { qCDebug(ARK) << "Opening internal viewer"; ArkViewer *internalViewer = new ArkViewer(); internalViewer->show(); if (internalViewer->viewInInternalViewer(fileName, mimeType)) { // The internal viewer is showing the file, and will // remove the temporary file in its destructor. So there // is no more to do here. return; } else { KMessageBox::sorry(nullptr, i18n("The internal viewer cannot preview this file.")); delete internalViewer; } } // Only get here if there is no internal viewer available or could be // used for the file, and no external viewer was opened. Nothing can be // done with the temporary file, so remove it now. qCDebug(ARK) << "Removing temporary file:" << fileName; QFile::remove(fileName); } bool ArkViewer::viewInInternalViewer(const QString& fileName, const QMimeType &mimeType) { setWindowFilePath(fileName); // Set icon and comment for the mimetype. m_iconLabel->setPixmap(QIcon::fromTheme(mimeType.iconName()).pixmap(IconSize(KIconLoader::Small), IconSize(KIconLoader::Small))); m_commentLabel->setText(mimeType.comment()); // Create the ReadOnlyPart instance. m_part = KMimeTypeTrader::self()->createPartInstanceFromQuery(mimeType.name(), this, this); // Drop the KHTMLPart, if necessary. const KService::Ptr service = KMimeTypeTrader::self()->preferredService(mimeType.name(), QStringLiteral("KParts/ReadOnlyPart")); qCDebug(ARK) << "Preferred service for mimetype" << mimeType.name() << "is" << service->library(); if (service.constData()->desktopEntryName() == QLatin1String("khtml")) { KService::List offers = KMimeTypeTrader::self()->query(mimeType.name(), QStringLiteral("KParts/ReadOnlyPart")); offers.removeFirst(); qCDebug(ARK) << "Removed KHTMLPart from the offers for mimetype" << mimeType.name() << ". Using" << offers.first().constData()->desktopEntryName() << "instead."; m_part = offers.first().constData()->createInstance(this, this); } if (!m_part.data()) { return false; } // Insert the KPart into its placeholder. centralWidget()->layout()->replaceWidget(m_partPlaceholder, m_part.data()->widget()); createGUI(m_part.data()); setAutoSaveSettings(QStringLiteral("Viewer"), true); m_part.data()->openUrl(QUrl::fromLocalFile(fileName)); m_part.data()->widget()->setFocus(); return true; } KService::Ptr ArkViewer::getViewer(const QString &mimeType) { // No point in even trying to find anything for application/octet-stream if (mimeType == QStringLiteral("application/octet-stream")) { return KService::Ptr(); } // Try to get a read-only kpart for the internal viewer KService::List offers = KMimeTypeTrader::self()->query(mimeType, QStringLiteral("KParts/ReadOnlyPart")); auto arkPartIt = std::find_if(offers.begin(), offers.end(), [](KService::Ptr service) { return service->storageId() == QLatin1String("ark_part.desktop"); }); // Use the Ark part only when the mime type matches an archive type directly. // Many file types (e.g. Open Document) are technically just archives // but browsing their internals is typically not what the user wants. if (arkPartIt != offers.end()) { // Not using hasMimeType() as we're explicitly not interested in inheritance. if (!(*arkPartIt)->mimeTypes().contains(mimeType)) { offers.erase(arkPartIt); } } // If we can't find a kpart, try to get an external application if (offers.isEmpty()) { offers = KMimeTypeTrader::self()->query(mimeType, QStringLiteral("Application")); } if (!offers.isEmpty()) { return offers.first(); } else { return KService::Ptr(); } }