diff --git a/autotests/kerfuffle/jsonarchiveinterface.cpp b/autotests/kerfuffle/jsonarchiveinterface.cpp index cd557c96..39ac097f 100644 --- a/autotests/kerfuffle/jsonarchiveinterface.cpp +++ b/autotests/kerfuffle/jsonarchiveinterface.cpp @@ -1,118 +1,126 @@ /* * Copyright (c) 2010-2011 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "jsonarchiveinterface.h" #include #include "kerfuffle/archiveentry.h" JSONArchiveInterface::JSONArchiveInterface(QObject *parent, const QVariantList& args) : Kerfuffle::ReadWriteArchiveInterface(parent, args) { } JSONArchiveInterface::~JSONArchiveInterface() { } bool JSONArchiveInterface::list() { JSONParser::JSONArchive::const_iterator it = m_archive.constBegin(); for (; it != m_archive.constEnd(); ++it) { emit entry(*it); } return true; } bool JSONArchiveInterface::open() { QFile file(filename()); if (!file.exists()) { return false; } if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { return false; } m_archive = JSONParser::parse(&file); return !m_archive.isEmpty(); } -bool JSONArchiveInterface::addFiles(const QList &files, const Kerfuffle::Archive::Entry *destination, const QString &tempDirPath, const Kerfuffle::CompressionOptions& options) +bool JSONArchiveInterface::addFiles(const QList& files, const Kerfuffle::Archive::Entry *destination, const Kerfuffle::CompressionOptions& options) { - Q_UNUSED(tempDirPath) Q_UNUSED(options) foreach (const Kerfuffle::Archive::Entry *entry, files) { const QString &path = destination->property("fullPath").toString() + entry->property("fullPath").toString(); if (m_archive.contains(path)) { return false; } Kerfuffle::Archive::Entry *e = new Kerfuffle::Archive::Entry(Q_NULLPTR); e->setProperty("fullPath", path); m_archive[path] = e; } return true; } +bool JSONArchiveInterface::moveFiles(const QList& files, const Kerfuffle::Archive::Entry *destination, const Kerfuffle::ExtractionOptions& options) +{ + Q_UNUSED(files) + Q_UNUSED(destination) + Q_UNUSED(options) + + return true; +} + bool JSONArchiveInterface::copyFiles(const QList& files, const QString& destinationDirectory, const Kerfuffle::ExtractionOptions& options) { Q_UNUSED(files) Q_UNUSED(destinationDirectory) Q_UNUSED(options) return true; } bool JSONArchiveInterface::deleteFiles(const QList& files) { foreach (const Kerfuffle::Archive::Entry *file, files) { const QString &fileName = file->property("fullPath").toString(); if (m_archive.contains(fileName)) { m_archive.remove(fileName); emit entryRemoved(fileName); } } return true; } bool JSONArchiveInterface::addComment(const QString& comment) { Q_UNUSED(comment) return true; } bool JSONArchiveInterface::testArchive() { return true; } diff --git a/autotests/kerfuffle/jsonarchiveinterface.h b/autotests/kerfuffle/jsonarchiveinterface.h index f9b12305..fa86b416 100644 --- a/autotests/kerfuffle/jsonarchiveinterface.h +++ b/autotests/kerfuffle/jsonarchiveinterface.h @@ -1,68 +1,69 @@ /* * Copyright (c) 2010-2011 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef JSONARCHIVEINTERFACE_H #define JSONARCHIVEINTERFACE_H #include "jsonparser.h" #include "kerfuffle/archiveinterface.h" #include "kerfuffle/archive_kerfuffle.h" /** * A dummy archive interface used by our test cases. * * It reads a JSON file which defines the contents of the archive. * For the file format description, see the documentation for @c JSONParser. * * The file's content is read to memory when open() is called and the archive * is then closed. This means that this class never changes the file's content * on disk, and entry addition or deletion do not change the original file. * * @sa JSONParser * * @author Raphael Kubo da Costa */ class JSONArchiveInterface : public Kerfuffle::ReadWriteArchiveInterface { Q_OBJECT public: explicit JSONArchiveInterface(QObject *parent, const QVariantList& args); virtual ~JSONArchiveInterface(); virtual bool list() Q_DECL_OVERRIDE; virtual bool open() Q_DECL_OVERRIDE; - virtual bool addFiles(const QList &files, const Kerfuffle::Archive::Entry *destination, const QString &tempDirPath, const Kerfuffle::CompressionOptions& options) Q_DECL_OVERRIDE; + virtual bool addFiles(const QList& files, const Kerfuffle::Archive::Entry *destination, const Kerfuffle::CompressionOptions& options) Q_DECL_OVERRIDE; + virtual bool moveFiles(const QList& files, const Kerfuffle::Archive::Entry *destination, const Kerfuffle::CompressionOptions& options) Q_DECL_OVERRIDE; virtual bool copyFiles(const QList& files, const QString& destinationDirectory, const Kerfuffle::ExtractionOptions& options) Q_DECL_OVERRIDE; virtual bool deleteFiles(const QList& files) Q_DECL_OVERRIDE; virtual bool addComment(const QString& comment) Q_DECL_OVERRIDE; virtual bool testArchive() Q_DECL_OVERRIDE; private: JSONParser::JSONArchive m_archive; }; #endif diff --git a/kerfuffle/archiveinterface.h b/kerfuffle/archiveinterface.h index 9dd32846..e9f7a3c2 100644 --- a/kerfuffle/archiveinterface.h +++ b/kerfuffle/archiveinterface.h @@ -1,163 +1,164 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef ARCHIVEINTERFACE_H #define ARCHIVEINTERFACE_H #include "archive_kerfuffle.h" #include "archive_entry.h" #include "kerfuffle_export.h" #include "kerfuffle/archiveentry.h" #include #include #include #include namespace Kerfuffle { class Query; class KERFUFFLE_EXPORT ReadOnlyArchiveInterface: public QObject { Q_OBJECT public: explicit ReadOnlyArchiveInterface(QObject *parent, const QVariantList & args); virtual ~ReadOnlyArchiveInterface(); /** * Returns the filename of the archive currently being handled. */ QString filename() const; /** * Returns the comment of the archive. */ QString comment() const; /** * @return The password of the archive, if any. */ QString password() const; /** * Returns whether the file can only be read. * * @return @c true The file cannot be written. * @return @c false The file can be read and written. */ virtual bool isReadOnly() const; virtual bool open(); /** * List archive contents. * This runs the process of reading archive contents. * When subclassing, you can block as long as you need (unless you called setWaitForFinishedSignal(true)). * @returns whether the listing succeeded. * @note If returning false, make sure to emit the error() signal beforewards to notify * the user of the error condition. */ virtual bool list() = 0; virtual bool testArchive() = 0; void setPassword(const QString &password); void setHeaderEncryptionEnabled(bool enabled); /** * Extract files from archive. * Globally recognized extraction options: * @li PreservePaths - preserve file paths (extract flat if false) * @li RootNode - node in the archive which will correspond to the @arg destinationDirectory * When subclassing, you can block as long as you need (unless you called setWaitForFinishedSignal(true)). * @returns whether the listing succeeded. * @note If returning false, make sure to emit the error() signal beforewards to notify * the user of the error condition. */ virtual bool copyFiles(const QList &files, const QString &destinationDirectory, const ExtractionOptions &options) = 0; bool waitForFinishedSignal(); /** * @return The list of filenames retrieved from the list of entries. */ static QStringList entryFullPaths(const QList &entries); virtual bool doKill(); virtual bool doSuspend(); virtual bool doResume(); bool isHeaderEncryptionEnabled() const; signals: void cancelled(); void error(const QString &message, const QString &details = QString()); void entry(Archive::Entry *archiveEntry); void entryRemoved(const QString &path); void progress(double progress); void info(const QString &info); void finished(bool result); void userQuery(Query *query); void testSuccess(); protected: /** * Setting this option to true will not run the functions in their own thread. * Instead it will be necessary to call finished(bool) when the operation is actually finished. */ void setWaitForFinishedSignal(bool value); void setCorrupt(bool isCorrupt); bool isCorrupt() const; QString m_comment; private: QString m_filename; QString m_password; bool m_waitForFinishedSignal; bool m_isHeaderEncryptionEnabled; bool m_isCorrupt; }; class KERFUFFLE_EXPORT ReadWriteArchiveInterface: public ReadOnlyArchiveInterface { Q_OBJECT public: explicit ReadWriteArchiveInterface(QObject *parent, const QVariantList & args); virtual ~ReadWriteArchiveInterface(); bool isReadOnly() const Q_DECL_OVERRIDE; //see archive.h for a list of what the compressionoptions might //contain - virtual bool addFiles(const QList &files, const Archive::Entry *destination, const QString &tempDirPath, const CompressionOptions& options) = 0; + virtual bool addFiles(const QList &files, const Archive::Entry *destination, const CompressionOptions& options) = 0; + virtual bool moveFiles(const QList &files, const Archive::Entry *destination, const CompressionOptions& options) = 0; virtual bool deleteFiles(const QList &files) = 0; virtual bool addComment(const QString &comment) = 0; }; } // namespace Kerfuffle #endif // ARCHIVEINTERFACE_H diff --git a/kerfuffle/cliinterface.cpp b/kerfuffle/cliinterface.cpp index 55a8078f..ee01ed00 100644 --- a/kerfuffle/cliinterface.cpp +++ b/kerfuffle/cliinterface.cpp @@ -1,1292 +1,1300 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "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 namespace Kerfuffle { CliInterface::CliInterface(QObject *parent, const QVariantList & args) : ReadWriteArchiveInterface(parent, args), m_process(0), m_listEmptyLines(false), m_abortingOperation(false), m_extractTempDir(Q_NULLPTR), m_commentTempFile(Q_NULLPTR) { //because this interface uses the event loop setWaitForFinishedSignal(true); if (QMetaType::type("QProcess::ExitStatus") == 0) { qRegisterMetaType("QProcess::ExitStatus"); } } void CliInterface::cacheParameterList() { m_param = parameterList(); Q_ASSERT(m_param.contains(ExtractProgram)); Q_ASSERT(m_param.contains(ListProgram)); Q_ASSERT(m_param.contains(PreservePathSwitch)); Q_ASSERT(m_param.contains(FileExistsExpression)); Q_ASSERT(m_param.contains(FileExistsInput)); } CliInterface::~CliInterface() { Q_ASSERT(!m_process); delete m_commentTempFile; } void CliInterface::setListEmptyLines(bool emptyLines) { m_listEmptyLines = emptyLines; } bool CliInterface::list() { resetParsing(); cacheParameterList(); m_operationMode = List; const auto args = substituteListVariables(m_param.value(ListArgs).toStringList(), password()); if (!runProcess(m_param.value(ListProgram).toStringList(), args)) { return false; } return true; } bool CliInterface::copyFiles(const QList &files, const QString &destinationDirectory, const ExtractionOptions &options) { qCDebug(ARK) << Q_FUNC_INFO << "to" << destinationDirectory; cacheParameterList(); m_operationMode = Copy; m_compressionOptions = options; m_copiedFiles = files; m_extractDestDir = destinationDirectory; const QStringList extractArgs = m_param.value(ExtractArgs).toStringList(); if (extractArgs.contains(QStringLiteral("$PasswordSwitch")) && options.value(QStringLiteral("PasswordProtectedHint")).toBool() && password().isEmpty()) { qCDebug(ARK) << "Password hint enabled, querying user"; if (!passwordQuery()) { return false; } } // Populate the argument list. const QStringList args = substituteCopyVariables(extractArgs, files, options.value(QStringLiteral("PreservePaths")).toBool(), password()); QUrl destDir = QUrl(destinationDirectory); QDir::setCurrent(destDir.adjusted(QUrl::RemoveScheme).url()); bool useTmpExtractDir = options.value(QStringLiteral("DragAndDrop")).toBool() || options.value(QStringLiteral("AlwaysUseTmpDir")).toBool(); if (useTmpExtractDir) { Q_ASSERT(!m_extractTempDir); m_extractTempDir = new QTemporaryDir(QApplication::applicationName() + QLatin1Char('-')); 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; } m_oldWorkingDir = QDir::currentPath(); destDir = QUrl(m_extractTempDir->path()); QDir::setCurrent(destDir.adjusted(QUrl::RemoveScheme).url()); } if (!runProcess(m_param.value(ExtractProgram).toStringList(), args)) { return false; } return true; } -bool CliInterface::addFiles(const QList &files, const Archive::Entry *destination, const QString &tempDirPath, const CompressionOptions& options) +bool CliInterface::addFiles(const QList &files, const Archive::Entry *destination, const CompressionOptions& options) { cacheParameterList(); m_operationMode = Add; const QStringList addArgs = m_param.value(AddArgs).toStringList(); QList filesToPass = QList(); // 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->property("fullPath").toString(); if (!destinationPath.isEmpty()) { - const QString absoluteDestinationPath = tempDirPath + QLatin1Char('/') + destinationPath; + const QTemporaryDir tempDir; + const QString absoluteDestinationPath = tempDir.path() + QLatin1Char('/') + destinationPath; QDir qDir; qDir.mkpath(absoluteDestinationPath); QObject *preservedParent = Q_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 == Q_NULLPTR) { preservedParent = file->parent(); } const QString filePath = QDir::currentPath() + QLatin1Char('/') + file->property("fullPath").toString(); QString newFilePath = absoluteDestinationPath + file->property("fullPath").toString(); // Symlink function can't accept the second argument as a path with trailing slash. if (newFilePath[newFilePath.count() - 1] == QLatin1Char('/')) { newFilePath.remove(newFilePath.count() - 1, 1); } if (!QFile::link(filePath, newFilePath)) { qCDebug(ARK) << "Can't create symlink" << filePath << newFilePath; return false; } else { qCDebug(ARK) << "Symlink's created:" << filePath << newFilePath; } } - qCDebug(ARK) << "Changing working dir again to " << tempDirPath; - QDir::setCurrent(tempDirPath); + qCDebug(ARK) << "Changing working dir again to " << tempDir.path(); + QDir::setCurrent(tempDir.path()); // Pass the entry without trailing slash for RAR compatibility. filesToPass.push_back(new Archive::Entry(preservedParent, destinationPath.left(destinationPath.count() - 1))); } else { filesToPass = files; } if (addArgs.contains(QStringLiteral("$PasswordSwitch")) && options.value(QStringLiteral("PasswordProtectedHint")).toBool() && password().isEmpty()) { qCDebug(ARK) << "Password hint enabled, querying user"; if (!passwordQuery()) { return false; } } int compLevel = options.value(QStringLiteral("CompressionLevel"), -1).toInt(); const auto args = substituteAddVariables(m_param.value(AddArgs).toStringList(), filesToPass, password(), isHeaderEncryptionEnabled(), compLevel); return runProcess(m_param.value(AddProgram).toStringList(), args); } +bool CliInterface::moveFiles(const QList &files, + const Archive::Entry *destination, + const CompressionOptions &options) +{ + return false; +} + bool CliInterface::deleteFiles(const QList &files) { cacheParameterList(); m_operationMode = Delete; m_removedFiles = files; const auto deleteArgs = m_param.value(DeleteArgs).toStringList(); const auto args = substituteDeleteVariables(deleteArgs, files, password()); return runProcess(m_param.value(DeleteProgram).toStringList(), args); } bool CliInterface::testArchive() { resetParsing(); cacheParameterList(); m_operationMode = Test; const auto args = substituteTestVariables(m_param.value(TestArgs).toStringList()); return runProcess(m_param.value(TestProgram).toStringList(), args); } bool CliInterface::runProcess(const QStringList& programNames, const QStringList& arguments) { Q_ASSERT(!m_process); QString programPath; for (int i = 0; i < programNames.count(); i++) { programPath = QStandardPaths::findExecutable(programNames.at(i)); if (!programPath.isEmpty()) break; } if (programPath.isEmpty()) { const QString names = programNames.join(QStringLiteral(", ")); emit error(xi18ncp("@info", "Failed to locate program %2 on disk.", "Failed to locate programs %2 on disk.", programNames.count(), names)); 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, SIGNAL(readyReadStandardOutput()), SLOT(readStdout()), Qt::DirectConnection); if (m_operationMode == Copy) { // Extraction jobs need a dedicated post-processing function. connect(m_process, static_cast(&KPtyProcess::finished), this, &CliInterface::copyProcessFinished, Qt::DirectConnection); } else { connect(m_process, static_cast(&KPtyProcess::finished), this, &CliInterface::processFinished, Qt::DirectConnection); } 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 = Q_NULLPTR; } // #193908 - #222392 // Don't emit finished() if the job was killed quietly. if (m_abortingOperation) { return; } if (m_operationMode == Delete) { QStringList removedFullPaths = entryFullPaths(m_removedFiles); foreach (const QString &fullPath, removedFullPaths) { emit entryRemoved(fullPath); } } if (m_operationMode == Add) { list(); } else if (m_operationMode == List && isCorrupt()) { Kerfuffle::LoadCorruptQuery query(filename()); emit userQuery(&query); query.waitForResponse(); 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::copyProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) { Q_ASSERT(m_operationMode == Copy); 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 = Q_NULLPTR; } if (m_compressionOptions.value(QStringLiteral("AlwaysUseTmpDir")).toBool()) { // 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()); } copyProcessCleanup(); emit finished(false); return; } if (!m_compressionOptions.value(QStringLiteral("DragAndDrop")).toBool()) { if (!moveToDestination(QDir::current(), QDir(m_extractDestDir), m_compressionOptions[QStringLiteral("PreservePaths")].toBool())) { 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_copiedFiles.size())); copyProcessCleanup(); emit finished(false); return; } copyProcessCleanup(); } } if (m_compressionOptions.value(QStringLiteral("DragAndDrop")).toBool()) { if (!moveDroppedFilesToDest(m_copiedFiles, m_extractDestDir)) { 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_copiedFiles.size())); copyProcessCleanup(); emit finished(false); return; } copyProcessCleanup(); } emit progress(1.0); emit finished(true); } bool CliInterface::moveDroppedFilesToDest(const QList &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->property("fullPath").toString().remove(file->rootNode)); QFileInfo absSourceEntry(QDir::current().absolutePath() + QLatin1Char('/') + file->property("fullPath").toString()); 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); emit userQuery(&query); query.waitForResponse(); 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::copyProcessCleanup() { if (!m_oldWorkingDir.isEmpty()) { QDir::setCurrent(m_oldWorkingDir); } if (m_extractTempDir) { delete m_extractTempDir; m_extractTempDir = Q_NULLPTR; } } 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::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); emit userQuery(&query); query.waitForResponse(); 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; } QStringList CliInterface::substituteListVariables(const QStringList &listArgs, const QString &password) { // Required if we call this function from unit tests. cacheParameterList(); QStringList args; foreach (const QString& arg, listArgs) { if (arg == QLatin1String("$Archive")) { args << filename(); continue; } if (arg == QLatin1String("$PasswordSwitch")) { args << passwordSwitch(password); continue; } // Simple argument (e.g. -slt in 7z), nothing to substitute, just add it to the list. args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } QStringList CliInterface::substituteCopyVariables(const QStringList &extractArgs, const QList &entries, bool preservePaths, const QString &password) { // Required if we call this function from unit tests. cacheParameterList(); QStringList args; foreach (const QString& arg, extractArgs) { qCDebug(ARK) << "Processing argument" << arg; if (arg == QLatin1String("$Archive")) { args << filename(); continue; } if (arg == QLatin1String("$PreservePathSwitch")) { args << preservePathSwitch(preservePaths); continue; } if (arg == QLatin1String("$PasswordSwitch")) { args << passwordSwitch(password); continue; } if (arg == QLatin1String("$Files")) { args << copyFilesList(entries); continue; } // Simple argument (e.g. -kb in unrar), nothing to substitute, just add it to the list. args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } QStringList CliInterface::substituteAddVariables(const QStringList &addArgs, const QList &entries, const QString &password, bool encryptHeader, int compLevel) { // Required if we call this function from unit tests. cacheParameterList(); QStringList args; foreach (const QString& arg, addArgs) { qCDebug(ARK) << "Processing argument " << arg; if (arg == QLatin1String("$Archive")) { args << filename(); continue; } if (arg == QLatin1String("$PasswordSwitch")) { args << (encryptHeader ? passwordHeaderSwitch(password) : passwordSwitch(password)); continue; } if (arg == QLatin1String("$CompressionLevelSwitch")) { args << compressionLevelSwitch(compLevel); continue; } if (arg == QLatin1String("$Files")) { args << entryFullPaths(entries); continue; } // Simple argument (e.g. a in 7z), nothing to substitute, just add it to the list. args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } QStringList CliInterface::substituteDeleteVariables(const QStringList &deleteArgs, const QList &entries, const QString &password) { cacheParameterList(); QStringList args; foreach (const QString& arg, deleteArgs) { qCDebug(ARK) << "Processing argument" << arg; if (arg == QLatin1String("$Archive")) { args << filename(); continue; } if (arg == QLatin1String("$PasswordSwitch")) { args << passwordSwitch(password); continue; } if (arg == QLatin1String("$Files")) { foreach (const Archive::Entry *e, entries) { args << escapeFileName(e->property("fullPath").toString()); } continue; } // Simple argument (e.g. d in rar), nothing to substitute, just add it to the list. args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } QStringList CliInterface::substituteCommentVariables(const QStringList &commentArgs, const QString &commentFile) { // Required if we call this function from unit tests. cacheParameterList(); QStringList args; foreach (const QString& arg, commentArgs) { qCDebug(ARK) << "Processing argument " << arg; if (arg == QLatin1String("$Archive")) { args << filename(); continue; } if (arg == QLatin1String("$CommentSwitch")) { QString commentSwitch = m_param.value(CommentSwitch).toString(); commentSwitch.replace(QStringLiteral("$CommentFile"), commentFile); args << commentSwitch; continue; } args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } QStringList CliInterface::substituteTestVariables(const QStringList &testArgs) { // Required if we call this function from unit tests. cacheParameterList(); QStringList args; foreach (const QString& arg, testArgs) { qCDebug(ARK) << "Processing argument " << arg; if (arg == QLatin1String("$Archive")) { args << filename(); continue; } args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } QString CliInterface::preservePathSwitch(bool preservePaths) const { Q_ASSERT(m_param.contains(PreservePathSwitch)); const QStringList theSwitch = m_param.value(PreservePathSwitch).toStringList(); Q_ASSERT(theSwitch.size() == 2); return (preservePaths ? theSwitch.at(0) : theSwitch.at(1)); } QStringList CliInterface::passwordHeaderSwitch(const QString& password) const { if (password.isEmpty()) { return QStringList(); } Q_ASSERT(m_param.contains(PasswordHeaderSwitch)); QStringList passwordHeaderSwitch = m_param.value(PasswordHeaderSwitch).toStringList(); Q_ASSERT(!passwordHeaderSwitch.isEmpty() && passwordHeaderSwitch.size() <= 2); if (passwordHeaderSwitch.size() == 1) { passwordHeaderSwitch[0].replace(QLatin1String("$Password"), password); } else { passwordHeaderSwitch[1] = password; } return passwordHeaderSwitch; } QStringList CliInterface::passwordSwitch(const QString& password) const { if (password.isEmpty()) { return QStringList(); } Q_ASSERT(m_param.contains(PasswordSwitch)); QStringList passwordSwitch = m_param.value(PasswordSwitch).toStringList(); Q_ASSERT(!passwordSwitch.isEmpty() && passwordSwitch.size() <= 2); if (passwordSwitch.size() == 1) { passwordSwitch[0].replace(QLatin1String("$Password"), password); } else { passwordSwitch[1] = password; } return passwordSwitch; } QString CliInterface::compressionLevelSwitch(int level) const { if (level < 0 || level > 9) { return QString(); } Q_ASSERT(m_param.contains(CompressionLevelSwitch)); QString compLevelSwitch = m_param.value(CompressionLevelSwitch).toString(); Q_ASSERT(!compLevelSwitch.isEmpty()); compLevelSwitch.replace(QLatin1String("$CompressionLevel"), QString::number(level)); return compLevelSwitch; } QStringList CliInterface::copyFilesList(const QList &entries) const { QStringList filesList; foreach (const Archive::Entry *e, entries) { filesList << escapeFileName(e->property("fullPath").toString()); } 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()); emit userQuery(&query); query.waitForResponse(); 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::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 = checkForErrorMessage(QLatin1String( lines.last() ), WrongPasswordPatterns); bool foundErrorMessage = (wrongPasswordMessage || checkForErrorMessage(QLatin1String(lines.last()), DiskFullPatterns) || checkForErrorMessage(QLatin1String(lines.last()), ExtractionFailedPatterns) || checkForPasswordPromptMessage(QLatin1String(lines.last())) || checkForErrorMessage(QLatin1String(lines.last()), FileExistsExpression)); 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)) { handleLine(QString::fromLocal8Bit(line)); } } } void 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 == Copy || m_operationMode == Add) && m_param.contains(CaptureProgress) && m_param.value(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; } } if (m_operationMode == Copy) { if (checkForPasswordPromptMessage(line)) { qCDebug(ARK) << "Found a password prompt"; Kerfuffle::PasswordNeededQuery query(filename()); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); killProcess(); return; } setPassword(query.password()); const QString response(password() + QLatin1Char('\n')); writeToProcess(response.toLocal8Bit()); return; } if (checkForErrorMessage(line, DiskFullPatterns)) { qCWarning(ARK) << "Found disk full message:" << line; emit error(i18nc("@info", "Extraction failed because the disk is full.")); killProcess(); return; } if (checkForErrorMessage(line, WrongPasswordPatterns)) { qCWarning(ARK) << "Wrong password!"; setPassword(QString()); emit error(i18nc("@info", "Extraction failed: Incorrect password")); killProcess(); return; } if (checkForErrorMessage(line, ExtractionFailedPatterns)) { qCWarning(ARK) << "Error in extraction:" << line; emit error(i18n("Extraction failed because of an unexpected error.")); killProcess(); return; } if (handleFileExistsMessage(line)) { return; } } if (m_operationMode == List) { if (checkForPasswordPromptMessage(line)) { qCDebug(ARK) << "Found a password prompt"; Kerfuffle::PasswordNeededQuery query(filename()); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); killProcess(); return; } setPassword(query.password()); const QString response(password() + QLatin1Char('\n')); writeToProcess(response.toLocal8Bit()); return; } if (checkForErrorMessage(line, WrongPasswordPatterns)) { qCWarning(ARK) << "Wrong password!"; setPassword(QString()); emit error(i18n("Incorrect password.")); killProcess(); return; } if (checkForErrorMessage(line, ExtractionFailedPatterns)) { qCWarning(ARK) << "Error in extraction!!"; emit error(i18n("Extraction failed because of an unexpected error.")); killProcess(); return; } if (checkForErrorMessage(line, CorruptArchivePatterns)) { qCWarning(ARK) << "Archive corrupt"; setCorrupt(true); return; } if (handleFileExistsMessage(line)) { return; } readListLine(line); return; } if (m_operationMode == Test) { if (checkForPasswordPromptMessage(line)) { qCDebug(ARK) << "Found a password prompt"; emit error(i18n("Ark does not currently support testing password-protected archives.")); killProcess(); return; } if (checkForTestSuccessMessage(line)) { qCDebug(ARK) << "Test successful"; emit testSuccess(); return; } } } bool CliInterface::checkForPasswordPromptMessage(const QString& line) { const QString passwordPromptPattern(m_param.value(PasswordPromptPattern).toString()); if (passwordPromptPattern.isEmpty()) return false; if (m_passwordPromptPattern.pattern().isEmpty()) { m_passwordPromptPattern.setPattern(m_param.value(PasswordPromptPattern).toString()); } if (m_passwordPromptPattern.match(line).hasMatch()) { return true; } return false; } bool CliInterface::handleFileExistsMessage(const QString& line) { // Check for a filename and store it. foreach (const QString &pattern, m_param.value(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 (!checkForErrorMessage(line, FileExistsExpression)) { return false; } Kerfuffle::OverwriteQuery query(QDir::current().path() + QLatin1Char( '/' ) + m_storedFileName); query.setNoRenameMode(true); emit userQuery(&query); qCDebug(ARK) << "Waiting response"; query.waitForResponse(); qCDebug(ARK) << "Finished response"; QString responseToProcess; const QStringList choices = m_param.value(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()) { 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::checkForErrorMessage(const QString& line, int parameterIndex) { QList patterns; if (m_patternCache.contains(parameterIndex)) { patterns = m_patternCache.value(parameterIndex); } else { if (!m_param.contains(parameterIndex)) { return false; } foreach(const QString& rawPattern, m_param.value(parameterIndex).toStringList()) { patterns << QRegularExpression(rawPattern); } m_patternCache[parameterIndex] = patterns; } foreach(const QRegularExpression& pattern, patterns) { if (pattern.match(line).hasMatch()) { return true; } } return false; } bool CliInterface::checkForTestSuccessMessage(const QString& line) { const QRegularExpression rx(m_param.value(TestPassedPattern).toString()); const QRegularExpressionMatch rxMatch = rx.match(line); if (rxMatch.hasMatch()) { return true; } return false; } bool CliInterface::doKill() { if (m_process) { killProcess(false); return true; } return false; } bool CliInterface::doSuspend() { return false; } bool CliInterface::doResume() { return false; } QString CliInterface::escapeFileName(const QString& fileName) const { return fileName; } 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) { cacheParameterList(); m_operationMode = Comment; m_commentTempFile = new QTemporaryFile; if (!m_commentTempFile->open()) { qCWarning(ARK) << "Failed to create temporary file for comment"; emit finished(false); return false; } QTextStream stream(m_commentTempFile); stream << comment << endl; m_commentTempFile->close(); const auto args = substituteCommentVariables(m_param.value(CommentArgs).toStringList(), m_commentTempFile->fileName()); if (!runProcess(m_param.value(AddProgram).toStringList(), args)) { return false; } m_comment = comment; return true; } } diff --git a/kerfuffle/cliinterface.h b/kerfuffle/cliinterface.h index 6e8e7fea..abe9d69a 100644 --- a/kerfuffle/cliinterface.h +++ b/kerfuffle/cliinterface.h @@ -1,451 +1,452 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef CLIINTERFACE_H #define CLIINTERFACE_H #include "archiveinterface.h" #include "archiveentry.h" #include "kerfuffle_export.h" #include #include class KProcess; class KPtyProcess; class QDir; class QTemporaryDir; class QTemporaryFile; namespace Kerfuffle { enum CliInterfaceParameters { ///////////////[ COMMON ]///////////// /** * Bool (default false) * Will look for the %-sign in the stdout while working, in the form of * (2%, 14%, 35%, etc etc), and report progress based upon this */ CaptureProgress = 0, /** * QString * Default: empty * A regexp pattern that matches the program's password prompt. */ PasswordPromptPattern, ///////////////[ LIST ]///////////// /** * QStringList * The names to the program that will handle listing of this * archive (eg "rar"). Will be searched for in PATH */ ListProgram, /** * QStringList * The arguments that are passed to the program above for * listing the archive. Special strings that will be * substituted: * $Archive - the path of the archive */ ListArgs, /** * QStringList (default empty) * List of regexp patterns that indicate a corrupt archive. */ CorruptArchivePatterns, ///////////////[ EXTRACT ]///////////// /** * QStringList * The names to the program that will handle extracting of this * archive (eg "rar"). Will be searched for in PATH */ ExtractProgram, /** * QStringList * The arguments that are passed to the program above for * extracting the archive. Special strings that will be * substituted: * $Archive - the path of the archive * $Files - the files selected to be extracted, if any * $PreservePathSwitch - the flag for extracting with full paths * $PasswordSwitch - the switch setting the password. Note that this * will not be inserted unless the listing function has emitted an * entry with the IsPasswordProtected property set to true. */ ExtractArgs, /** * Bool (default false) * When passing directories to the extract program, do not * include trailing slashes * e.g. if the user selected "foo/" and "foo/bar" in the gui, the * paths "foo" and "foo/bar" will be sent to the program. */ NoTrailingSlashes, /** * QStringList * This should be a qstringlist with either two elements. The first * string is what PreservePathSwitch in the ExtractArgs will be replaced * with if PreservePath is True/enabled. The second is for the disabled * case. An empty string means that the argument will not be used in * that case. * Example: for rar, "x" means extract with full paths, and "e" means * extract without full paths. in this case we will use the stringlist * ("x", "e"). Or, for another format that might use the switch * "--extractFull" for preservePaths, and nothing otherwise: we use the * stringlist ("--extractFull", "") */ PreservePathSwitch, /** * QStringList (default empty) * The format of the root node switch. The variable $Password will be * substituted for the password string. NOTE: supplying passwords * through a virtual terminal is not supported (yet?), because this * is not cross platform compatible. As of KDE 4.3 there are no plans to * change this. * Example: ("-p$Password) * or ("--password", "$Password") */ PasswordSwitch, /** * QString * The format of the compression level switch. The variable $CompressionLevel * will be substituted for the level. * Example: ("-mx=$CompressionLevel) */ CompressionLevelSwitch, /** * QStringList * This is a stringlist with regexps, defining how to recognize the last * line in a "File already exists" prompt when extracting. */ FileExistsExpression, /** * QStringList * This is a stringlist with regexps defining how to recognize the line * containing the filename in a "File already exists" prompt when * extracting. It should have one captured string, which is the filename * of the file/folder that already exists. */ FileExistsFileName, /** * int * This sets on what output channel the FileExistsExpression regex * should be applied on, in other words, on what stream the "file * exists" output will appear in. Values accepted: * 0 - Standard error, stderr (default) * 1 - Standard output, stdout */ FileExistsMode, /** * QStringList * The various responses that can be supplied as a response to the * "file exists" prompt. The various items are to be supplied in the * following order: * index 0 - Yes (overwrite) * index 1 - No (skip/do not overwrite) * index 2 - All (overwrite all) * index 3 - Do not overwrite any files (autoskip) * index 4 - Cancel operation */ FileExistsInput, /** * QStringList * Regexp patterns capturing disk is full error messages. */ DiskFullPatterns, ///////////////[ DELETE ]///////////// /** * QStringList * The names to the program that will handle deleting of elements in this * archive format (eg "rar"). Will be searched for in PATH */ DeleteProgram, /** * QStringList * The arguments that are passed to the program above for * deleting from the archive. Special strings that will be * substituted: * $Archive - the path of the archive * $Files - the files selected to be deleted */ DeleteArgs, /** * QStringList * Default: empty * A list of regexp patterns that will cause the extraction to exit * with a general fail message */ ExtractionFailedPatterns, /** * QStringList * Default: empty * A list of regexp patterns that will alert the user that the password * was wrong. */ WrongPasswordPatterns, ///////////////[ ADD ]///////////// /** * QStringList * The names to the program that will handle adding in this * archive format (eg "rar"). Will be searched for in PATH */ AddProgram, /** * QStringList * The arguments that are passed to the program above for * adding to the archive. Special strings that will be * substituted: * $Archive - the path of the archive * $Files - the files selected to be added */ AddArgs, ///////////////[ ENCRYPT ]///////////// /** * QStringList (default empty) * The variable $Password will be * substituted for the password string used to encrypt the header. * Example (rar plugin): ("-hp$Password") */ PasswordHeaderSwitch, ///////////////[ COMMENT ]///////////// /** * QStringList * The arguments that are passed to AddProgram when adding * a comment. */ CommentArgs, /** * QString * The variable $CommentFile will be substituted for the file * containing the comment. * Example (rar plugin): -z$CommentFile */ CommentSwitch, TestProgram, TestArgs, TestPassedPattern }; typedef QHash ParameterList; class KERFUFFLE_EXPORT CliInterface : public ReadWriteArchiveInterface { Q_OBJECT public: enum OperationMode { List, Copy, Add, Delete, Comment, Test }; OperationMode m_operationMode; explicit CliInterface(QObject *parent, const QVariantList & args); virtual ~CliInterface(); virtual bool list() Q_DECL_OVERRIDE; virtual bool copyFiles(const QList &files, const QString& destinationDirectory, const ExtractionOptions& options) Q_DECL_OVERRIDE; - virtual bool addFiles(const QList &files, const Archive::Entry *destination, const QString &tempDirPath, const CompressionOptions& options) Q_DECL_OVERRIDE; + virtual bool addFiles(const QList &files, const Archive::Entry *destination, const CompressionOptions& options) Q_DECL_OVERRIDE; + virtual bool moveFiles(const QList &files, const Archive::Entry *destination, const CompressionOptions& options) Q_DECL_OVERRIDE; virtual bool deleteFiles(const QList &files) Q_DECL_OVERRIDE; virtual bool addComment(const QString &comment) Q_DECL_OVERRIDE; virtual bool testArchive() Q_DECL_OVERRIDE; virtual void resetParsing() = 0; virtual ParameterList parameterList() const = 0; virtual bool readListLine(const QString &line) = 0; bool doKill() Q_DECL_OVERRIDE; bool doSuspend() Q_DECL_OVERRIDE; bool doResume() Q_DECL_OVERRIDE; /** * Sets if the listing should include empty lines. * * The default value is false. */ void setListEmptyLines(bool emptyLines); /** * Move all files from @p tmpDir to @p destDir, preserving paths if @p preservePaths is true. * @return Whether the operation has been successful. */ bool moveToDestination(const QDir &tempDir, const QDir &destDir, bool preservePaths); QStringList substituteListVariables(const QStringList &listArgs, const QString &password); QStringList substituteCopyVariables(const QStringList &extractArgs, const QList &entries, bool preservePaths, const QString &password); QStringList substituteAddVariables(const QStringList &addArgs, const QList &entries, const QString &password, bool encryptHeader, int compLevel); QStringList substituteDeleteVariables(const QStringList &deleteArgs, const QList &entries, const QString &password); QStringList substituteCommentVariables(const QStringList &commentArgs, const QString &commentFile); QStringList substituteTestVariables(const QStringList &testArgs); /** * @return The preserve path switch, according to the @p preservePaths extraction option. */ QString preservePathSwitch(bool preservePaths) const; /** * @return The password header-switch with the given @p password. */ virtual QStringList passwordHeaderSwitch(const QString& password) const; /** * @return The password switch with the given @p password. */ QStringList passwordSwitch(const QString& password) const; /** * @return The compression level switch with the given @p level. */ QString compressionLevelSwitch(int level) const; /** * @return The list of selected files to extract. */ QStringList copyFilesList(const QList &files) const; protected: virtual void handleLine(const QString& line); virtual void cacheParameterList(); /** * Run @p programName with the given @p arguments. * * @param programName The program that will be run (not the whole path). * @param arguments A list of arguments that will be passed to the program. * * @return @c true if the program was found and the process was started correctly, * @c false otherwise (in which case finished(false) is emitted). */ bool runProcess(const QStringList& programNames, const QStringList& arguments); /** * Kill the running process. The finished signal is emitted according to @p emitFinished. */ void killProcess(bool emitFinished = true); /** * Ask the password *before* running any process. * @return True if the user supplies a password, false otherwise (in which case finished() is emitted). */ bool passwordQuery(); ParameterList m_param; int m_exitCode; protected slots: virtual void readStdout(bool handleAll = false); private: /** * Checks whether a line of the program's output is a password prompt. * * It uses the regular expression in the @c PasswordPromptPattern parameter * for the check. * * @param line A line of the program's output. * * @return @c true if the given @p line is a password prompt, @c false * otherwise. */ bool checkForPasswordPromptMessage(const QString& line); bool handleFileExistsMessage(const QString& filename); bool checkForErrorMessage(const QString& line, int parameterIndex); bool checkForTestSuccessMessage(const QString& line); /** * Performs any additional escaping and processing on @p fileName * before passing it to the underlying process. * * The default implementation returns @p fileName unchanged. * * @param fileName String to escape. */ virtual QString escapeFileName(const QString &fileName) const; /** * Wrapper around KProcess::write() or KPtyDevice::write(), depending on * the platform. */ void writeToProcess(const QByteArray& data); bool moveDroppedFilesToDest(const QList &files, const QString &finalDest); /** * @return Whether @p dir is an empty directory. */ bool isEmptyDir(const QDir &dir); void copyProcessCleanup(); QByteArray m_stdOutData; QRegularExpression m_passwordPromptPattern; QHash > m_patternCache; #ifdef Q_OS_WIN KProcess *m_process; #else KPtyProcess *m_process; #endif QList m_removedFiles; bool m_listEmptyLines; bool m_abortingOperation; QString m_storedFileName; CompressionOptions m_compressionOptions; QString m_oldWorkingDir; QString m_extractDestDir; QTemporaryDir *m_extractTempDir; QTemporaryFile *m_commentTempFile; QList m_copiedFiles; private slots: void processFinished(int exitCode, QProcess::ExitStatus exitStatus); void copyProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); }; } #endif /* CLIINTERFACE_H */ diff --git a/kerfuffle/jobs.cpp b/kerfuffle/jobs.cpp index 0b0efbbc..ef73df18 100644 --- a/kerfuffle/jobs.cpp +++ b/kerfuffle/jobs.cpp @@ -1,564 +1,601 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "jobs.h" #include "archiveentry.h" #include "ark_debug.h" #include #include #include #include #include #include //#define DEBUG_RACECONDITION namespace Kerfuffle { class Job::Private : public QThread { public: Private(Job *job, QObject *parent = 0) : QThread(parent) , q(job) { connect(q, &KJob::result, this, &QThread::quit); } virtual void run() Q_DECL_OVERRIDE; private: Job *q; }; void Job::Private::run() { q->doWork(); if (q->isRunning()) { exec(); } #ifdef DEBUG_RACECONDITION QThread::sleep(2); #endif } Job::Job(ReadOnlyArchiveInterface *interface) : KJob() , m_archiveInterface(interface) , m_isRunning(false) , d(new Private(this)) { static bool onlyOnce = false; if (!onlyOnce) { qRegisterMetaType >("QPair"); onlyOnce = true; } setCapabilities(KJob::Killable); } Job::~Job() { qDeleteAll(m_archiveEntries); m_archiveEntries.clear(); if (d->isRunning()) { d->wait(); } delete d; } ReadOnlyArchiveInterface *Job::archiveInterface() { return m_archiveInterface; } bool Job::isRunning() const { return m_isRunning; } void Job::start() { jobTimer.start(); m_isRunning = true; if (archiveInterface()->waitForFinishedSignal()) { // CLI-based interfaces run a QProcess, no need to use threads. QTimer::singleShot(0, this, &Job::doWork); } else { // Run the job in another thread. d->start(); } } void Job::emitResult() { m_isRunning = false; KJob::emitResult(); } void Job::connectToArchiveInterfaceSignals() { connect(archiveInterface(), &ReadOnlyArchiveInterface::cancelled, this, &Job::onCancelled); connect(archiveInterface(), &ReadOnlyArchiveInterface::error, this, &Job::onError); connect(archiveInterface(), &ReadOnlyArchiveInterface::entry, this, &Job::onEntry); connect(archiveInterface(), &ReadOnlyArchiveInterface::entryRemoved, this, &Job::onEntryRemoved); connect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &Job::onProgress); connect(archiveInterface(), &ReadOnlyArchiveInterface::info, this, &Job::onInfo); connect(archiveInterface(), &ReadOnlyArchiveInterface::finished, this, &Job::onFinished, Qt::DirectConnection); connect(archiveInterface(), &ReadOnlyArchiveInterface::userQuery, this, &Job::onUserQuery); } void Job::onCancelled() { qCDebug(ARK) << "Cancelled emitted"; setError(KJob::KilledJobError); } void Job::onError(const QString & message, const QString & details) { Q_UNUSED(details) qCDebug(ARK) << "Error emitted:" << message; setError(KJob::UserDefinedError); setErrorText(message); } void Job::onEntry(Archive::Entry *entry) { emit newEntry(entry); } void Job::onProgress(double value) { setPercent(static_cast(100.0*value)); } void Job::onInfo(const QString& info) { emit infoMessage(this, info); } void Job::onEntryRemoved(const QString & path) { emit entryRemoved(path); } void Job::onFinished(bool result) { qCDebug(ARK) << "Job finished, result:" << result << ", time:" << jobTimer.elapsed() << "ms"; emitResult(); } void Job::onUserQuery(Query *query) { emit userQuery(query); } bool Job::doKill() { bool ret = archiveInterface()->doKill(); if (!ret) { qCWarning(ARK) << "Killing does not seem to be supported here."; } return ret; } ListJob::ListJob(ReadOnlyArchiveInterface *interface) : Job(interface) , m_isSingleFolderArchive(true) , m_isPasswordProtected(false) , m_extractedFilesSize(0) , m_dirCount(0) , m_filesCount(0) { qCDebug(ARK) << "ListJob started"; connect(this, &ListJob::newEntry, this, &ListJob::onNewEntry); } void ListJob::doWork() { emit description(this, i18n("Loading archive...")); connectToArchiveInterfaceSignals(); bool ret = archiveInterface()->list(); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } qlonglong ListJob::extractedFilesSize() const { return m_extractedFilesSize; } bool ListJob::isPasswordProtected() const { return m_isPasswordProtected; } bool ListJob::isSingleFolderArchive() const { if (m_filesCount == 1 && m_dirCount == 0) { return false; } return m_isSingleFolderArchive; } void ListJob::onNewEntry(const Archive::Entry *entry) { m_extractedFilesSize += entry->property("size").toLongLong(); m_isPasswordProtected |= entry->property("isPasswordProtected").toBool(); if (entry->isDir()) { m_dirCount++; } else { m_filesCount++; } if (m_isSingleFolderArchive) { // RPM filenames have the ./ prefix, and "." would be detected as the subfolder name, so we remove it. const QString fullPath = entry->property("fullPath").toString().replace(QRegularExpression(QStringLiteral("^\\./")), QString()); const QString basePath = fullPath.split(QLatin1Char('/')).at(0); if (m_basePath.isEmpty()) { m_basePath = basePath; m_subfolderName = basePath; } else { if (m_basePath != basePath) { m_isSingleFolderArchive = false; m_subfolderName.clear(); } } } } QString ListJob::subfolderName() const { if (!isSingleFolderArchive()) { return QString(); } return m_subfolderName; } ExtractJob::ExtractJob(const QList &entries, const QString &destinationDir, const ExtractionOptions &options, ReadOnlyArchiveInterface *interface) : Job(interface) , m_entries(entries) , m_destinationDir(destinationDir) , m_options(options) { qCDebug(ARK) << "ExtractJob created"; setDefaultOptions(); } void ExtractJob::doWork() { QString desc; if (m_entries.count() == 0) { desc = i18n("Extracting all files"); } else { desc = i18np("Extracting one file", "Extracting %1 files", m_entries.count()); } emit description(this, desc); QFileInfo destDirInfo(m_destinationDir); if (destDirInfo.isDir() && (!destDirInfo.isWritable() || !destDirInfo.isExecutable())) { onError(xi18n("Could not write to destination %1.Check whether you have sufficient permissions.", m_destinationDir), QString()); onFinished(false); return; } connectToArchiveInterfaceSignals(); qCDebug(ARK) << "Starting extraction with selected files:" << m_entries << "Destination dir:" << m_destinationDir << "Options:" << m_options; bool ret = archiveInterface()->copyFiles(m_entries, m_destinationDir, m_options); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void ExtractJob::setDefaultOptions() { ExtractionOptions defaultOptions; defaultOptions[QStringLiteral("PreservePaths")] = false; ExtractionOptions::const_iterator it = defaultOptions.constBegin(); for (; it != defaultOptions.constEnd(); ++it) { if (!m_options.contains(it.key())) { m_options[it.key()] = it.value(); } } } QString ExtractJob::destinationDirectory() const { return m_destinationDir; } ExtractionOptions ExtractJob::extractionOptions() const { return m_options; } TempExtractJob::TempExtractJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : Job(interface) , m_entry(entry) , m_passwordProtectedHint(passwordProtectedHint) { } QString TempExtractJob::validatedFilePath() const { QString path = extractionDir() + QLatin1Char('/') + m_entry->property("fullPath").toString(); // Make sure a maliciously crafted archive with parent folders named ".." do // not cause the previewed file path to be located outside the temporary // directory, resulting in a directory traversal issue. path.remove(QStringLiteral("../")); return path; } ExtractionOptions TempExtractJob::extractionOptions() const { ExtractionOptions options; options[QStringLiteral("PreservePaths")] = true; if (m_passwordProtectedHint) { options[QStringLiteral("PasswordProtectedHint")] = true; } return options; } void TempExtractJob::doWork() { emit description(this, i18n("Extracting one file")); connectToArchiveInterfaceSignals(); qCDebug(ARK) << "Extracting:" << m_entry; bool ret = archiveInterface()->copyFiles({ m_entry }, extractionDir(), extractionOptions()); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } PreviewJob::PreviewJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : TempExtractJob(entry, passwordProtectedHint, interface) { qCDebug(ARK) << "PreviewJob started"; } QString PreviewJob::extractionDir() const { return m_tmpExtractDir.path(); } OpenJob::OpenJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : TempExtractJob(entry, passwordProtectedHint, interface) { qCDebug(ARK) << "OpenJob started"; m_tmpExtractDir = new QTemporaryDir(); } QTemporaryDir *OpenJob::tempDir() const { return m_tmpExtractDir; } QString OpenJob::extractionDir() const { return m_tmpExtractDir->path(); } OpenWithJob::OpenWithJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : OpenJob(entry, passwordProtectedHint, interface) { qCDebug(ARK) << "OpenWithJob started"; } AddJob::AddJob(const QList &entries, const Archive::Entry *destination, const CompressionOptions& options , ReadWriteArchiveInterface *interface) : Job(interface) , m_entries(entries) , m_destination(destination) , m_options(options) { qCDebug(ARK) << "AddJob started"; } void AddJob::doWork() { qCDebug(ARK) << "AddJob: going to add" << m_entries.count() << "file(s)"; emit description(this, i18np("Adding a file", "Adding %1 files", m_entries.count())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); const QString globalWorkDir = m_options.value(QStringLiteral("GlobalWorkDir")).toString(); const QDir workDir = globalWorkDir.isEmpty() ? QDir::current() : QDir(globalWorkDir); if (!globalWorkDir.isEmpty()) { qCDebug(ARK) << "GlobalWorkDir is set, changing dir to " << globalWorkDir; m_oldWorkingDir = QDir::currentPath(); QDir::setCurrent(globalWorkDir); } // The file paths must be relative to GlobalWorkDir. foreach (Archive::Entry *entry, m_entries) { // #191821: workDir must be used instead of QDir::current() // so that symlinks aren't resolved automatically const QString &fullPath = entry->property("fullPath").toString(); QString relativePath = workDir.relativeFilePath(fullPath); if (fullPath.endsWith(QLatin1Char('/'))) { relativePath += QLatin1Char('/'); } qCDebug(ARK) << entry->property("fullPath") << entry->isDir() << relativePath; entry->setFullPath(relativePath); } connectToArchiveInterfaceSignals(); - bool ret = m_writeInterface->addFiles(m_entries, m_destination, m_tmpExtractDir.path(), m_options); + bool ret = m_writeInterface->addFiles(m_entries, m_destination, m_options); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void AddJob::onFinished(bool result) { if (!m_oldWorkingDir.isEmpty()) { QDir::setCurrent(m_oldWorkingDir); } Job::onFinished(result); } +MoveJob::MoveJob(const QList &entries, const Archive::Entry *destination, const CompressionOptions& options , ReadWriteArchiveInterface *interface) + : Job(interface) + , m_entries(entries) + , m_destination(destination) + , m_options(options) +{ + qCDebug(ARK) << "MoveJob started"; +} + +void MoveJob::doWork() +{ + qCDebug(ARK) << "MoveJob: going to move" << m_entries.count() << "file(s)"; + + emit description(this, i18np("Moving a file", "Moving %1 files", m_entries.count())); + + 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) +{ + if (!m_oldWorkingDir.isEmpty()) { + QDir::setCurrent(m_oldWorkingDir); + } + + Job::onFinished(result); +} + DeleteJob::DeleteJob(const QList &entries, ReadWriteArchiveInterface *interface) : Job(interface) , m_entries(entries) { } void DeleteJob::doWork() { emit description(this, i18np("Deleting a file from the archive", "Deleting %1 files", m_entries.count())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->deleteFiles(m_entries); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } CommentJob::CommentJob(const QString& comment, ReadWriteArchiveInterface *interface) : Job(interface) , m_comment(comment) { } void CommentJob::doWork() { emit description(this, i18n("Adding comment")); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->addComment(m_comment); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } TestJob::TestJob(ReadOnlyArchiveInterface *interface) : Job(interface) { m_testSuccess = false; } void TestJob::doWork() { qCDebug(ARK) << "TestJob started"; emit description(this, i18n("Testing archive")); connectToArchiveInterfaceSignals(); connect(archiveInterface(), &ReadOnlyArchiveInterface::testSuccess, this, &TestJob::onTestSuccess); bool ret = archiveInterface()->testArchive(); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void TestJob::onTestSuccess() { m_testSuccess = true; } bool TestJob::testSucceeded() { return m_testSuccess; } } // namespace Kerfuffle diff --git a/kerfuffle/jobs.h b/kerfuffle/jobs.h index 2f7f294d..1d1930d7 100644 --- a/kerfuffle/jobs.h +++ b/kerfuffle/jobs.h @@ -1,292 +1,312 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef JOBS_H #define JOBS_H #include "kerfuffle_export.h" #include "archiveinterface.h" #include "archive_kerfuffle.h" #include "archiveentry.h" #include "queries.h" #include #include #include namespace Kerfuffle { class KERFUFFLE_EXPORT Job : public KJob { Q_OBJECT public: void start(); bool isRunning() const; protected: Job(ReadOnlyArchiveInterface *interface); virtual ~Job(); virtual bool doKill(); virtual void emitResult(); ReadOnlyArchiveInterface *archiveInterface(); QList m_archiveEntries; void connectToArchiveInterfaceSignals(); public slots: virtual void doWork() = 0; protected slots: virtual void onCancelled(); virtual void onError(const QString &message, const QString &details); virtual void onInfo(const QString &info); virtual void onEntry(Archive::Entry *entry); virtual void onProgress(double progress); virtual void onEntryRemoved(const QString &path); virtual void onFinished(bool result); virtual void onUserQuery(Query *query); signals: void entryRemoved(const QString & entry); void error(const QString& errorMessage, const QString& details); void newEntry(Archive::Entry*); void userQuery(Kerfuffle::Query*); private: ReadOnlyArchiveInterface *m_archiveInterface; bool m_isRunning; QElapsedTimer jobTimer; class Private; Private * const d; }; class KERFUFFLE_EXPORT ListJob : public Job { Q_OBJECT public: explicit ListJob(ReadOnlyArchiveInterface *interface); qlonglong extractedFilesSize() const; bool isPasswordProtected() const; bool isSingleFolderArchive() const; QString subfolderName() const; public slots: virtual void doWork() Q_DECL_OVERRIDE; private: bool m_isSingleFolderArchive; bool m_isPasswordProtected; QString m_subfolderName; QString m_basePath; qlonglong m_extractedFilesSize; qlonglong m_dirCount; qlonglong m_filesCount; private slots: void onNewEntry(const Archive::Entry*); }; class KERFUFFLE_EXPORT ExtractJob : public Job { Q_OBJECT public: ExtractJob(const QList &entries, const QString& destinationDir, const ExtractionOptions& options, ReadOnlyArchiveInterface *interface); QString destinationDirectory() const; ExtractionOptions extractionOptions() const; public slots: virtual void doWork() Q_DECL_OVERRIDE; private: // TODO: Maybe this should be a method if ExtractionOptions were a class? void setDefaultOptions(); QList m_entries; QString m_destinationDir; ExtractionOptions m_options; }; /** * Abstract base class for jobs that extract a single file to a temporary dir. * It's not possible to pass extraction options and paths will be always preserved. * The only option that the job needs to know is whether the file is password protected. */ class KERFUFFLE_EXPORT TempExtractJob : public Job { Q_OBJECT public: TempExtractJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface); /** * @return The absolute path of the extracted file. * The path is validated in order to prevent directory traversal attacks. */ QString validatedFilePath() const; ExtractionOptions extractionOptions() const; public slots: virtual void doWork() Q_DECL_OVERRIDE; private: virtual QString extractionDir() const = 0; Archive::Entry *m_entry; bool m_passwordProtectedHint; }; /** * This TempExtractJob can be used to preview a file. * The temporary extraction directory will be deleted upon job's completion. */ class KERFUFFLE_EXPORT PreviewJob : public TempExtractJob { Q_OBJECT public: PreviewJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface); private: QString extractionDir() const Q_DECL_OVERRIDE; QTemporaryDir m_tmpExtractDir; }; /** * This TempExtractJob can be used to open a file in its dedicated application. * For this reason, the temporary extraction directory will NOT be deleted upon job's completion. */ class KERFUFFLE_EXPORT OpenJob : public TempExtractJob { Q_OBJECT public: OpenJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface); /** * @return The temporary dir used for the extraction. * It is safe to delete this pointer in order to remove the directory. */ QTemporaryDir *tempDir() const; private: QString extractionDir() const Q_DECL_OVERRIDE; QTemporaryDir *m_tmpExtractDir; }; class KERFUFFLE_EXPORT OpenWithJob : public OpenJob { Q_OBJECT public: OpenWithJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface); }; class KERFUFFLE_EXPORT AddJob : public Job { Q_OBJECT public: AddJob(const QList &files, const Archive::Entry *destination, const CompressionOptions& options, ReadWriteArchiveInterface *interface); public slots: virtual void doWork() Q_DECL_OVERRIDE; protected slots: virtual void onFinished(bool result) Q_DECL_OVERRIDE; +private: + QString m_oldWorkingDir; + const QList m_entries; + const Archive::Entry *m_destination; + CompressionOptions m_options; +}; + +class KERFUFFLE_EXPORT MoveJob : public Job +{ +Q_OBJECT + +public: + MoveJob(const QList &files, const Archive::Entry *destination, const CompressionOptions& options, ReadWriteArchiveInterface *interface); + +public slots: + virtual void doWork() Q_DECL_OVERRIDE; + +protected slots: + virtual void onFinished(bool result) Q_DECL_OVERRIDE; + private: QTemporaryDir m_tmpExtractDir; QString m_oldWorkingDir; const QList m_entries; const Archive::Entry *m_destination; CompressionOptions m_options; }; class KERFUFFLE_EXPORT DeleteJob : public Job { Q_OBJECT public: DeleteJob(const QList &files, ReadWriteArchiveInterface *interface); public slots: virtual void doWork() Q_DECL_OVERRIDE; private: QList m_entries; }; class KERFUFFLE_EXPORT CommentJob : public Job { Q_OBJECT public: CommentJob(const QString& comment, ReadWriteArchiveInterface *interface); public slots: virtual void doWork() Q_DECL_OVERRIDE; private: QString m_comment; }; class KERFUFFLE_EXPORT TestJob : public Job { Q_OBJECT public: TestJob(ReadOnlyArchiveInterface *interface); bool testSucceeded(); public slots: virtual void doWork() Q_DECL_OVERRIDE; private slots: virtual void onTestSuccess(); private: bool m_testSuccess; }; } // namespace Kerfuffle #endif // JOBS_H diff --git a/plugins/libarchive/libarchiveplugin.cpp b/plugins/libarchive/libarchiveplugin.cpp index 0a3a274a..49b72f3d 100644 --- a/plugins/libarchive/libarchiveplugin.cpp +++ b/plugins/libarchive/libarchiveplugin.cpp @@ -1,513 +1,522 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2010 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 "libarchiveplugin.h" #include "kerfuffle/queries.h" #include #include LibarchivePlugin::LibarchivePlugin(QObject *parent, const QVariantList & args) : ReadWriteArchiveInterface(parent, args) , m_archiveReadDisk(archive_read_disk_new()) , m_abortOperation(false) , m_cachedArchiveEntryCount(0) , m_emitNoEntries(false) , m_extractedFilesSize(0) { qCDebug(ARK) << "Initializing libarchive plugin"; archive_read_disk_set_standard_lookup(m_archiveReadDisk.data()); } LibarchivePlugin::~LibarchivePlugin() { } bool LibarchivePlugin::list() { qCDebug(ARK) << "Listing archive contents"; ArchiveRead arch_reader(archive_read_new()); if (!initializeReader(arch_reader)) { return false; } qDebug(ARK) << "Detected compression filter:" << archive_filter_name(arch_reader.data(), 0); m_cachedArchiveEntryCount = 0; m_extractedFilesSize = 0; struct archive_entry *aentry; int result = ARCHIVE_RETRY; bool firstEntry = true; while (!m_abortOperation && (result = archive_read_next_header(arch_reader.data(), &aentry)) == ARCHIVE_OK) { if (firstEntry) { qDebug(ARK) << "Detected format for first entry:" << archive_format_name(arch_reader.data()); firstEntry = false; } if (!m_emitNoEntries) { emitEntryFromArchiveEntry(aentry); } m_extractedFilesSize += (qlonglong)archive_entry_size(aentry); m_cachedArchiveEntryCount++; archive_read_data_skip(arch_reader.data()); } m_abortOperation = false; if (result != ARCHIVE_EOF) { const QString errorString = QLatin1String(archive_error_string(arch_reader.data())); // FIXME: what about the other archive_error_string() calls? Do they also happen to return empty strings? emit error(errorString.isEmpty() ? i18nc("@info", "Could not read until the end of the archive") : errorString); return false; } return archive_read_close(arch_reader.data()) == ARCHIVE_OK; } -bool LibarchivePlugin::addFiles(const QList &files, const Archive::Entry *destination, const QString &tempDirPath, const CompressionOptions& options) +bool LibarchivePlugin::addFiles(const QList &files, const Archive::Entry *destination, const CompressionOptions& options) +{ + Q_UNUSED(files) + Q_UNUSED(destination) + Q_UNUSED(options) + return false; +} + +bool LibarchivePlugin::moveFiles(const QList &files, + const Archive::Entry *destination, + const CompressionOptions &options) { Q_UNUSED(files) Q_UNUSED(destination) - Q_UNUSED(tempDirPath) Q_UNUSED(options) return false; } bool LibarchivePlugin::deleteFiles(const QList &files) { Q_UNUSED(files) return false; } bool LibarchivePlugin::addComment(const QString& comment) { Q_UNUSED(comment) return false; } bool LibarchivePlugin::testArchive() { return false; } bool LibarchivePlugin::doKill() { m_abortOperation = true; return true; } bool LibarchivePlugin::copyFiles(const QList& files, const QString& destinationDirectory, const ExtractionOptions& options) { qCDebug(ARK) << "Changing current directory to " << destinationDirectory; QDir::setCurrent(destinationDirectory); const bool extractAll = files.isEmpty(); const bool preservePaths = options.value(QStringLiteral( "PreservePaths" )).toBool(); bool removeRootNode = options.value(QStringLiteral("RemoveRootNode"), QVariant()).toBool(); // To avoid traversing the entire archive when extracting a limited set of // entries, we maintain a list of remaining entries and stop when it's // empty. QStringList fullPaths = entryFullPaths(files); QStringList remainingFiles = entryFullPaths(files); ArchiveRead arch(archive_read_new()); if (!initializeReader(arch)) { return false; } ArchiveWrite writer(archive_write_disk_new()); if (!writer.data()) { return false; } archive_write_disk_set_options(writer.data(), extractionFlags()); int entryNr = 0; int totalCount = 0; if (extractAll) { if (!m_cachedArchiveEntryCount) { emit progress(0); //TODO: once information progress has been implemented, send //feedback here that the archive is being read qCDebug(ARK) << "For getting progress information, the archive will be listed once"; m_emitNoEntries = true; list(); m_emitNoEntries = false; } totalCount = m_cachedArchiveEntryCount; } else { totalCount = files.size(); } qCDebug(ARK) << "Going to extract" << totalCount << "entries"; // Initialize variables. bool overwriteAll = false; // Whether to overwrite all files bool skipAll = false; // Whether to skip all files bool dontPromptErrors = false; // Whether to prompt for errors m_currentExtractedFilesSize = 0; int no_entries = 0; struct archive_entry *entry; QString fileBeingRenamed; // Iterate through all entries in archive. while (!m_abortOperation && (archive_read_next_header(arch.data(), &entry) == ARCHIVE_OK)) { if (!extractAll && remainingFiles.isEmpty()) { break; } fileBeingRenamed.clear(); int index = -1; // Retry with renamed entry, fire an overwrite query again // if the new entry also exists. retry: const bool entryIsDir = S_ISDIR(archive_entry_mode(entry)); // Skip directories if not preserving paths. if (!preservePaths && entryIsDir) { archive_read_data_skip(arch.data()); continue; } // entryName is the name inside the archive, full path QString entryName = QDir::fromNativeSeparators(QFile::decodeName(archive_entry_pathname(entry))); // For now we just can't handle absolute filenames in a tar archive. // TODO: find out what to do here!! if (entryName.startsWith(QLatin1Char( '/' ))) { emit error(i18n("This archive contains archive entries with absolute paths, " "which are not supported by Ark.")); return false; } // Should the entry be extracted? if (extractAll || remainingFiles.contains(entryName) || entryName == fileBeingRenamed) { // Find the index of entry. if (entryName != fileBeingRenamed) { index = fullPaths.indexOf(entryName); } if (!extractAll && index == -1) { // If entry is not found in files, skip entry. continue; } // entryFI is the fileinfo pointing to where the file will be // written from the archive. QFileInfo entryFI(entryName); //qCDebug(ARK) << "setting path to " << archive_entry_pathname( entry ); const QString fileWithoutPath(entryFI.fileName()); // If we DON'T preserve paths, we cut the path and set the entryFI // fileinfo to the one without the path. if (!preservePaths) { // Empty filenames (ie dirs) should have been skipped already, // so asserting. Q_ASSERT(!fileWithoutPath.isEmpty()); archive_entry_copy_pathname(entry, QFile::encodeName(fileWithoutPath).constData()); entryFI = QFileInfo(fileWithoutPath); // OR, if the file has a rootNode attached, remove it from file path. } else if (!extractAll && removeRootNode && entryName != fileBeingRenamed) { const QString &rootNode = files.at(index)->rootNode; if (!rootNode.isEmpty()) { //qCDebug(ARK) << "Removing" << files.at(index).value().rootNode << "from" << entryName; const QString truncatedFilename(entryName.remove(0, rootNode.size())); archive_entry_copy_pathname(entry, QFile::encodeName(truncatedFilename).constData()); entryFI = QFileInfo(truncatedFilename); } } // Check if the file about to be written already exists. if (!entryIsDir && entryFI.exists()) { if (skipAll) { archive_read_data_skip(arch.data()); archive_entry_clear(entry); continue; } else if (!overwriteAll && !skipAll) { Kerfuffle::OverwriteQuery query(entryName); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { archive_read_data_skip(arch.data()); archive_entry_clear(entry); break; } else if (query.responseSkip()) { archive_read_data_skip(arch.data()); archive_entry_clear(entry); continue; } else if (query.responseAutoSkip()) { archive_read_data_skip(arch.data()); archive_entry_clear(entry); skipAll = true; continue; } else if (query.responseRename()) { const QString newName(query.newFilename()); fileBeingRenamed = newName; archive_entry_copy_pathname(entry, QFile::encodeName(newName).constData()); goto retry; } else if (query.responseOverwriteAll()) { overwriteAll = true; } } } // If there is an already existing directory. if (entryIsDir && entryFI.exists()) { if (entryFI.isWritable()) { qCWarning(ARK) << "Warning, existing, but writable dir"; } else { qCWarning(ARK) << "Warning, existing, but non-writable dir. skipping"; archive_entry_clear(entry); archive_read_data_skip(arch.data()); continue; } } // Write the entry header and check return value. const int returnCode = archive_write_header(writer.data(), entry); switch (returnCode) { case ARCHIVE_OK: // If the whole archive is extracted and the total filesize is // available, we use partial progress. copyData(entryName, arch.data(), writer.data(), (extractAll && m_extractedFilesSize)); break; case ARCHIVE_FAILED: qCCritical(ARK) << "archive_write_header() has returned" << returnCode << "with errno" << archive_errno(writer.data()); // If they user previously decided to ignore future errors, // don't bother prompting again. if (!dontPromptErrors) { // Ask the user if he wants to continue extraction despite an error for this entry. Kerfuffle::ContinueExtractionQuery query(QLatin1String(archive_error_string(writer.data())), entryName); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); return false; } dontPromptErrors = query.dontAskAgain(); } break; case ARCHIVE_FATAL: qCCritical(ARK) << "archive_write_header() has returned" << returnCode << "with errno" << archive_errno(writer.data()); emit error(xi18nc("@info", "Extraction failed at:%1", entryName)); return false; default: qCDebug(ARK) << "archive_write_header() returned" << returnCode << "which will be ignored."; break; } // If we only partially extract the archive and the number of // archive entries is available we use a simple progress based on // number of items extracted. if (!extractAll && m_cachedArchiveEntryCount) { ++entryNr; emit progress(float(entryNr) / totalCount); } archive_entry_clear(entry); no_entries++; remainingFiles.removeOne(entryName); } else { // Archive entry not among selected files, skip it. archive_read_data_skip(arch.data()); } } // While entries left to read in archive. m_abortOperation = false; qCDebug(ARK) << "Extracted" << no_entries << "entries"; return archive_read_close(arch.data()) == ARCHIVE_OK; } bool LibarchivePlugin::initializeReader(const LibarchivePlugin::ArchiveRead &archiveRead) { if (!(archiveRead.data())) { emit error(i18n("The archive reader could not be initialized.")); return false; } if (archive_read_support_filter_all(archiveRead.data()) != ARCHIVE_OK) { return false; } if (archive_read_support_format_all(archiveRead.data()) != ARCHIVE_OK) { return false; } if (archive_read_open_filename(archiveRead.data(), QFile::encodeName(filename()), 10240) != ARCHIVE_OK) { emit error(xi18nc("@info", "Could not open the archive %1." "Check whether you have sufficient permissions.", filename())); return false; } return true; } void LibarchivePlugin::emitEntryFromArchiveEntry(struct archive_entry *aentry) { Archive::Entry *e = new Archive::Entry(Q_NULLPTR); #ifdef _MSC_VER e->setProperty("fullPath", QDir::fromNativeSeparators(QString::fromUtf16((ushort*)archive_entry_pathname_w(aentry)))); #else e->setProperty("fullPath", QDir::fromNativeSeparators(QString::fromWCharArray(archive_entry_pathname_w(aentry)))); #endif const QString owner = QString::fromLatin1(archive_entry_uname(aentry)); if (!owner.isEmpty()) { e->setProperty("owner", owner); } const QString group = QString::fromLatin1(archive_entry_gname(aentry)); if (!group.isEmpty()) { e->setProperty("group", group); } e->compressedSizeIsSet = false; e->setProperty("size", (qlonglong)archive_entry_size(aentry)); e->setProperty("isDirectory", S_ISDIR(archive_entry_mode(aentry))); if (archive_entry_symlink(aentry)) { e->setProperty("link", QLatin1String( archive_entry_symlink(aentry) )); } e->setProperty("timestamp", QDateTime::fromTime_t(archive_entry_mtime(aentry))); emit entry(e); } int LibarchivePlugin::extractionFlags() const { int result = ARCHIVE_EXTRACT_TIME; result |= ARCHIVE_EXTRACT_SECURE_NODOTDOT; // TODO: Don't use arksettings here /*if ( ArkSettings::preservePerms() ) { result &= ARCHIVE_EXTRACT_PERM; } if ( !ArkSettings::extractOverwrite() ) { result &= ARCHIVE_EXTRACT_NO_OVERWRITE; }*/ return result; } void LibarchivePlugin::copyData(const QString& filename, struct archive *dest, bool partialprogress) { char buff[10240]; ssize_t readBytes; QFile file(filename); if (!file.open(QIODevice::ReadOnly)) { return; } readBytes = file.read(buff, sizeof(buff)); while (readBytes > 0) { archive_write_data(dest, buff, readBytes); if (archive_errno(dest) != ARCHIVE_OK) { qCCritical(ARK) << "Error while writing" << filename << ":" << archive_error_string(dest) << "(error no =" << archive_errno(dest) << ')'; return; } if (partialprogress) { m_currentExtractedFilesSize += readBytes; emit progress(float(m_currentExtractedFilesSize) / m_extractedFilesSize); } readBytes = file.read(buff, sizeof(buff)); } file.close(); } void LibarchivePlugin::copyData(const QString& filename, struct archive *source, struct archive *dest, bool partialprogress) { char buff[10240]; ssize_t readBytes; readBytes = archive_read_data(source, buff, sizeof(buff)); while (readBytes > 0) { archive_write_data(dest, buff, readBytes); if (archive_errno(dest) != ARCHIVE_OK) { qCCritical(ARK) << "Error while extracting" << filename << ":" << archive_error_string(dest) << "(error no =" << archive_errno(dest) << ')'; return; } if (partialprogress) { m_currentExtractedFilesSize += readBytes; emit progress(float(m_currentExtractedFilesSize) / m_extractedFilesSize); } readBytes = archive_read_data(source, buff, sizeof(buff)); } } #include "libarchiveplugin.moc" diff --git a/plugins/libarchive/libarchiveplugin.h b/plugins/libarchive/libarchiveplugin.h index 612fd967..195b0ac4 100644 --- a/plugins/libarchive/libarchiveplugin.h +++ b/plugins/libarchive/libarchiveplugin.h @@ -1,97 +1,98 @@ /* * Copyright (c) 2007 Henrique Pinto * 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. */ #ifndef LIBARCHIVEPLUGIN_H #define LIBARCHIVEPLUGIN_H #include "kerfuffle/archiveinterface.h" #include "kerfuffle/archiveentry.h" #include #include using namespace Kerfuffle; class LibarchivePlugin : public ReadWriteArchiveInterface { Q_OBJECT public: explicit LibarchivePlugin(QObject *parent, const QVariantList& args); virtual ~LibarchivePlugin(); virtual bool list() Q_DECL_OVERRIDE; virtual bool doKill() Q_DECL_OVERRIDE; virtual bool copyFiles(const QList& files, const QString& destinationDirectory, const ExtractionOptions& options) Q_DECL_OVERRIDE; - virtual bool addFiles(const QList &files, const Archive::Entry *destination, const QString &tempDirPath, const CompressionOptions& options) Q_DECL_OVERRIDE; + virtual bool addFiles(const QList &files, const Archive::Entry *destination, const CompressionOptions& options) Q_DECL_OVERRIDE; + virtual bool moveFiles(const QList &files, const Archive::Entry *destination, const CompressionOptions& options) Q_DECL_OVERRIDE; virtual bool deleteFiles(const QList& files) Q_DECL_OVERRIDE; virtual bool addComment(const QString& comment) Q_DECL_OVERRIDE; virtual bool testArchive() Q_DECL_OVERRIDE; protected: struct ArchiveReadCustomDeleter { static inline void cleanup(struct archive *a) { if (a) { archive_read_free(a); } } }; struct ArchiveWriteCustomDeleter { static inline void cleanup(struct archive *a) { if (a) { archive_write_free(a); } } }; typedef QScopedPointer ArchiveRead; typedef QScopedPointer ArchiveWrite; bool initializeReader(const LibarchivePlugin::ArchiveRead &archiveRead); void emitEntryFromArchiveEntry(struct archive_entry *entry); void copyData(const QString& filename, struct archive *dest, bool partialprogress = true); void copyData(const QString& filename, struct archive *source, struct archive *dest, bool partialprogress = true); ArchiveRead m_archiveReadDisk; bool m_abortOperation; private: int extractionFlags() const; int m_cachedArchiveEntryCount; qlonglong m_currentExtractedFilesSize; bool m_emitNoEntries; qlonglong m_extractedFilesSize; }; #endif // LIBARCHIVEPLUGIN_H diff --git a/plugins/libarchive/readwritelibarchiveplugin.cpp b/plugins/libarchive/readwritelibarchiveplugin.cpp index 0dad6561..aa1783c7 100644 --- a/plugins/libarchive/readwritelibarchiveplugin.cpp +++ b/plugins/libarchive/readwritelibarchiveplugin.cpp @@ -1,460 +1,467 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2010 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 "readwritelibarchiveplugin.h" #include #include #include #include K_PLUGIN_FACTORY_WITH_JSON(ReadWriteLibarchivePluginFactory, "kerfuffle_libarchive.json", registerPlugin();) ReadWriteLibarchivePlugin::ReadWriteLibarchivePlugin(QObject *parent, const QVariantList & args) : LibarchivePlugin(parent, args) { qCDebug(ARK) << "Loaded libarchive read-write plugin"; } ReadWriteLibarchivePlugin::~ReadWriteLibarchivePlugin() { } -bool ReadWriteLibarchivePlugin::addFiles(const QList &files, const Archive::Entry *destination, const QString &tempDirPath, const CompressionOptions& options) +bool ReadWriteLibarchivePlugin::addFiles(const QList &files, const Archive::Entry *destination, const CompressionOptions& options) { qCDebug(ARK) << "Adding" << files.size() << "entries with CompressionOptions" << options; const bool creatingNewFile = !QFileInfo::exists(filename()); m_writtenFiles.clear(); ArchiveRead arch_reader; if (!creatingNewFile) { arch_reader.reset(archive_read_new()); if (!initializeReader(arch_reader)) { return false; } } // |tempFile| needs to be created before |arch_writer| so that when we go // out of scope in a `return false' case ArchiveWriteCustomDeleter is // called before destructor of QSaveFile (ie. we call archive_write_close() // before close()'ing the file descriptor). QSaveFile tempFile(filename()); if (!tempFile.open(QIODevice::WriteOnly | QIODevice::Unbuffered)) { emit error(xi18nc("@info", "Failed to create a temporary file to compress %1.", filename())); return false; } ArchiveWrite arch_writer(archive_write_new()); if (!(arch_writer.data())) { emit error(i18n("The archive writer could not be initialized.")); return false; } // pax_restricted is the libarchive default, let's go with that. archive_write_set_format_pax_restricted(arch_writer.data()); if (creatingNewFile) { if (!initializeNewFileWriter(arch_writer, options)) { return false; } } else { if (!initializeWriter(arch_writer, arch_reader)) { return false; } } if (archive_write_open_fd(arch_writer.data(), tempFile.handle()) != ARCHIVE_OK) { emit error(xi18nc("@info", "Opening the archive for writing failed with the following error:%1", QLatin1String(archive_error_string(arch_writer.data())))); return false; } // First write the new files. qCDebug(ARK) << "Writing new entries"; int no_entries = 0; // Recreate destination directory structure. const QString destinationPath = destination->property("fullPath").toString(); qCDebug(ARK) << destinationPath.split(QLatin1Char('/')); foreach(const QString& directory, destinationPath.split(QLatin1Char('/'))) { if (!directory.isEmpty()) { qCDebug(ARK) << directory; if (!writeFile(directory, QString(), arch_writer.data())) { return false; } no_entries++; } } foreach(Archive::Entry *selectedFile, files) { if (m_abortOperation) { break; } if (!writeFile(selectedFile->property("fullPath").toString(), destinationPath, arch_writer.data())) { return false; } no_entries++; // For directories, write all subfiles/folders. const QString &fullPath = selectedFile->property("fullPath").toString(); if (QFileInfo(fullPath).isDir()) { QDirIterator it(fullPath, QDir::AllEntries | QDir::Readable | QDir::Hidden | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (!m_abortOperation && it.hasNext()) { QString path = it.next(); if ((it.fileName() == QLatin1String("..")) || (it.fileName() == QLatin1String("."))) { continue; } const bool isRealDir = it.fileInfo().isDir() && !it.fileInfo().isSymLink(); if (isRealDir) { path.append(QLatin1Char('/')); } if (!writeFile(path, destinationPath, arch_writer.data())) { return false; } no_entries++; } } } qCDebug(ARK) << "Added" << no_entries << "new entries to archive"; // If we have old archive entries. if (!creatingNewFile) { qCDebug(ARK) << "Copying any old entries"; if (!copyOldEntries(arch_writer, arch_reader, m_writtenFiles, no_entries)) { QFile::remove(tempFile.fileName()); return false; } qCDebug(ARK) << "Added" << no_entries << "old entries to archive"; } m_abortOperation = false; // In the success case, we need to manually close the archive_writer before // calling QSaveFile::commit(), otherwise the latter will close() the // file descriptor archive_writer is still working on. // TODO: We need to abstract this code better so that we only deal with one // object that manages both QSaveFile and ArchiveWriter. archive_write_close(arch_writer.data()); tempFile.commit(); return true; } +bool ReadWriteLibarchivePlugin::moveFiles(const QList &files, + const Archive::Entry *destination, + const CompressionOptions &options) +{ + return false; +} + bool ReadWriteLibarchivePlugin::deleteFiles(const QList& files) { qCDebug(ARK) << "Deleting" << files.size() << "entries"; ArchiveRead arch_reader(archive_read_new()); if (!initializeReader(arch_reader)) { return false; } // |tempFile| needs to be created before |arch_writer| so that when we go // out of scope in a `return false' case ArchiveWriteCustomDeleter is // called before destructor of QSaveFile (ie. we call archive_write_close() // before close()'ing the file descriptor). QSaveFile tempFile(filename()); if (!tempFile.open(QIODevice::WriteOnly | QIODevice::Unbuffered)) { emit error(i18nc("@info", "Failed to create a temporary file.")); return false; } ArchiveWrite arch_writer(archive_write_new()); if (!(arch_writer.data())) { emit error(i18n("The archive writer could not be initialized.")); return false; } // pax_restricted is the libarchive default, let's go with that. archive_write_set_format_pax_restricted(arch_writer.data()); if (!initializeWriter(arch_writer, arch_reader)) { return false; } if (archive_write_open_fd(arch_writer.data(), tempFile.handle()) != ARCHIVE_OK) { emit error(xi18nc("@info", "Opening the archive for writing failed with the following error:" "%1", QLatin1String(archive_error_string(arch_writer.data())))); return false; } // Copy old elements from previous archive to new archive. int no_entries = 0; QStringList filePaths = entryFullPaths(files); copyOldEntries(arch_writer, arch_reader, filePaths, no_entries, true); qCDebug(ARK) << "Removed" << no_entries << "entries from archive"; // In the success case, we need to manually close the archive_writer before // calling QSaveFile::commit(), otherwise the latter will close() the // file descriptor archive_writer is still working on. // TODO: We need to abstract this code better so that we only deal with one // object that manages both QSaveFile and ArchiveWriter. archive_write_close(arch_writer.data()); tempFile.commit(); return true; } bool ReadWriteLibarchivePlugin::initializeNewFileWriter(const LibarchivePlugin::ArchiveWrite &archiveWrite, const CompressionOptions &options) { int ret; bool requiresExecutable = false; if (filename().right(2).toUpper() == QLatin1String("GZ")) { qCDebug(ARK) << "Detected gzip compression for new file"; ret = archive_write_add_filter_gzip(archiveWrite.data()); } else if (filename().right(3).toUpper() == QLatin1String("BZ2")) { qCDebug(ARK) << "Detected bzip2 compression for new file"; ret = archive_write_add_filter_bzip2(archiveWrite.data()); } else if (filename().right(2).toUpper() == QLatin1String("XZ")) { qCDebug(ARK) << "Detected xz compression for new file"; ret = archive_write_add_filter_xz(archiveWrite.data()); } else if (filename().right(4).toUpper() == QLatin1String("LZMA")) { qCDebug(ARK) << "Detected lzma compression for new file"; ret = archive_write_add_filter_lzma(archiveWrite.data()); } else if (filename().right(2).toUpper() == QLatin1String(".Z")) { qCDebug(ARK) << "Detected compress (.Z) compression for new file"; ret = archive_write_add_filter_compress(archiveWrite.data()); } else if (filename().right(2).toUpper() == QLatin1String("LZ")) { qCDebug(ARK) << "Detected lzip compression for new file"; ret = archive_write_add_filter_lzip(archiveWrite.data()); } else if (filename().right(3).toUpper() == QLatin1String("LZO")) { qCDebug(ARK) << "Detected lzop compression for new file"; ret = archive_write_add_filter_lzop(archiveWrite.data()); } else if (filename().right(3).toUpper() == QLatin1String("LRZ")) { qCDebug(ARK) << "Detected lrzip compression for new file"; ret = archive_write_add_filter_lrzip(archiveWrite.data()); requiresExecutable = true; #ifdef HAVE_LIBARCHIVE_3_2_0 } else if (filename().right(3).toUpper() == QLatin1String("LZ4")) { qCDebug(ARK) << "Detected lz4 compression for new file"; ret = archive_write_add_filter_lz4(archiveWrite.data()); #endif } else if (filename().right(3).toUpper() == QLatin1String("TAR")) { qCDebug(ARK) << "Detected no compression for new file (pure tar)"; ret = archive_write_add_filter_none(archiveWrite.data()); } else { qCDebug(ARK) << "Falling back to gzip"; ret = archive_write_add_filter_gzip(archiveWrite.data()); } // Libarchive emits a warning for lrzip due to using external executable. if ((requiresExecutable && ret != ARCHIVE_WARN) || (!requiresExecutable && ret != ARCHIVE_OK)) { emit error(xi18nc("@info", "Setting the compression method failed with the following error:%1", QLatin1String(archive_error_string(archiveWrite.data())))); return false; } // Set compression level if passed in CompressionOptions. if (options.contains(QStringLiteral("CompressionLevel"))) { qCDebug(ARK) << "Using compression level:" << options.value(QStringLiteral("CompressionLevel")).toString(); ret = archive_write_set_filter_option(archiveWrite.data(), NULL, "compression-level", options.value(QStringLiteral("CompressionLevel")).toString().toUtf8()); if (ret != ARCHIVE_OK) { qCWarning(ARK) << "Failed to set compression level"; emit error(xi18nc("@info", "Setting the compression level failed with the following error:%1", QLatin1String(archive_error_string(archiveWrite.data())))); return false; } } return true; } bool ReadWriteLibarchivePlugin::initializeWriter(const ArchiveWrite &archiveWrite, const ArchiveRead &archiveRead) { int ret; bool requiresExecutable = false; switch (archive_filter_code(archiveRead.data(), 0)) { case ARCHIVE_FILTER_GZIP: ret = archive_write_add_filter_gzip(archiveWrite.data()); break; case ARCHIVE_FILTER_BZIP2: ret = archive_write_add_filter_bzip2(archiveWrite.data()); break; case ARCHIVE_FILTER_XZ: ret = archive_write_add_filter_xz(archiveWrite.data()); break; case ARCHIVE_FILTER_LZMA: ret = archive_write_add_filter_lzma(archiveWrite.data()); break; case ARCHIVE_FILTER_COMPRESS: ret = archive_write_add_filter_compress(archiveWrite.data()); break; case ARCHIVE_FILTER_LZIP: ret = archive_write_add_filter_lzip(archiveWrite.data()); break; case ARCHIVE_FILTER_LZOP: ret = archive_write_add_filter_lzop(archiveWrite.data()); break; case ARCHIVE_FILTER_LRZIP: ret = archive_write_add_filter_lrzip(archiveWrite.data()); requiresExecutable = true; break; #ifdef HAVE_LIBARCHIVE_3_2_0 case ARCHIVE_FILTER_LZ4: ret = archive_write_add_filter_lz4(archiveWrite.data()); break; #endif case ARCHIVE_FILTER_NONE: ret = archive_write_add_filter_none(archiveWrite.data()); break; default: emit error(i18n("The compression type '%1' is not supported by Ark.", QLatin1String(archive_filter_name(archiveRead.data(), 0)))); return false; } // Libarchive emits a warning for lrzip due to using external executable. if ((requiresExecutable && ret != ARCHIVE_WARN) || (!requiresExecutable && ret != ARCHIVE_OK)) { emit error(xi18nc("@info", "Setting the compression method failed with the following error:%1", QLatin1String(archive_error_string(archiveWrite.data())))); return false; } return true; } bool ReadWriteLibarchivePlugin::copyOldEntries(const LibarchivePlugin::ArchiveWrite &archiveWrite, const LibarchivePlugin::ArchiveRead &archiveRead, const QStringList &filePaths, int &entriesCounter, bool deleteMode) { struct archive_entry *entry; entriesCounter = 0; while ((deleteMode || !m_abortOperation) && archive_read_next_header(archiveRead.data(), &entry) == ARCHIVE_OK) { QString file = QFile::decodeName(archive_entry_pathname(entry)); if (filePaths.contains(file)) { archive_read_data_skip(archiveRead.data()); if (deleteMode) { emit entryRemoved(file); entriesCounter++; } else { qCDebug(ARK) << file << "is already present in the new archive, skipping."; } continue; } const int returnCode = archive_write_header(archiveWrite.data(), entry); switch (returnCode) { case ARCHIVE_OK: // If the whole archive is extracted and the total filesize is // available, we use partial progress. copyData(QLatin1String(archive_entry_pathname(entry)), archiveRead.data(), archiveWrite.data(), false); if (!deleteMode) { entriesCounter++; } break; case ARCHIVE_FAILED: case ARCHIVE_FATAL: qCCritical(ARK) << "archive_write_header() has returned" << returnCode << "with errno" << archive_errno(archiveWrite.data()); emit error(xi18nc("@info", "Compression failed while processing:" "%1Operation aborted.", file)); return false; default: qCDebug(ARK) << "archive_writer_header() has returned" << returnCode << "which will be ignored."; break; } if (!deleteMode) { archive_entry_clear(entry); } } return true; } // TODO: if we merge this with copyData(), we can pass more data // such as an fd to archive_read_disk_entry_from_file() bool ReadWriteLibarchivePlugin::writeFile(const QString& relativeName, const QString& destination, struct archive* arch_writer) { int header_response; const QString absoluteFilename = QFileInfo(relativeName).absoluteFilePath(); const QString destinationFilename = destination + relativeName; // #253059: Even if we use archive_read_disk_entry_from_file, // libarchive may have been compiled without HAVE_LSTAT, // or something may have caused it to follow symlinks, in // which case stat() will be called. To avoid this, we // call lstat() ourselves. struct stat st; lstat(QFile::encodeName(absoluteFilename).constData(), &st); struct archive_entry *entry = archive_entry_new(); archive_entry_set_pathname(entry, QFile::encodeName(destinationFilename).constData()); archive_entry_copy_sourcepath(entry, QFile::encodeName(absoluteFilename).constData()); archive_read_disk_entry_from_file(m_archiveReadDisk.data(), entry, -1, &st); if ((header_response = archive_write_header(arch_writer, entry)) == ARCHIVE_OK) { // If the whole archive is extracted and the total filesize is // available, we use partial progress. copyData(absoluteFilename, arch_writer, false); } else { qCCritical(ARK) << "Writing header failed with error code " << header_response; qCCritical(ARK) << "Error while writing..." << archive_error_string(arch_writer) << "(error no =" << archive_errno(arch_writer) << ')'; emit error(xi18nc("@info Error in a message box", "Ark could not compress %1:%2", absoluteFilename, QString::fromUtf8(archive_error_string(arch_writer)))); archive_entry_free(entry); return false; } m_writtenFiles.push_back(destinationFilename); emitEntryFromArchiveEntry(entry); archive_entry_free(entry); return true; } #include "readwritelibarchiveplugin.moc" diff --git a/plugins/libarchive/readwritelibarchiveplugin.h b/plugins/libarchive/readwritelibarchiveplugin.h index 3c294e19..c59953b4 100644 --- a/plugins/libarchive/readwritelibarchiveplugin.h +++ b/plugins/libarchive/readwritelibarchiveplugin.h @@ -1,57 +1,58 @@ /* * Copyright (c) 2007 Henrique Pinto * 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. */ #ifndef READWRITELIBARCHIVEPLUGIN_H #define READWRITELIBARCHIVEPLUGIN_H #include "libarchiveplugin.h" #include #include using namespace Kerfuffle; class ReadWriteLibarchivePlugin : public LibarchivePlugin { Q_OBJECT public: explicit ReadWriteLibarchivePlugin(QObject *parent, const QVariantList& args); ~ReadWriteLibarchivePlugin(); - bool addFiles(const QList &files, const Archive::Entry *destination, const QString &tempDirPath, const CompressionOptions& options) Q_DECL_OVERRIDE; + bool addFiles(const QList &files, const Archive::Entry *destination, const CompressionOptions& options) Q_DECL_OVERRIDE; + bool moveFiles(const QList &files, const Archive::Entry *destination, const CompressionOptions& options) Q_DECL_OVERRIDE; bool deleteFiles(const QList& files) Q_DECL_OVERRIDE; private: bool initializeNewFileWriter(const ArchiveWrite &archiveWrite, const CompressionOptions &options); bool initializeWriter(const ArchiveWrite &archiveWrite, const ArchiveRead &archiveRead); bool copyOldEntries(const ArchiveWrite &archiveWrite, const ArchiveRead &archiveRead, const QStringList &filePaths, int &entriesCounter, bool deleteMode = false); bool writeFile(const QString& relativeName, const QString& destination, struct archive* arch); QStringList m_writtenFiles; }; #endif // READWRITELIBARCHIVEPLUGIN_H