diff --git a/kerfuffle/archiveentry.cpp b/kerfuffle/archiveentry.cpp index f3d17790..7064f692 100644 --- a/kerfuffle/archiveentry.cpp +++ b/kerfuffle/archiveentry.cpp @@ -1,177 +1,195 @@ // // Created by mvlabat on 5/27/16. // #include "archiveentry.h" namespace Kerfuffle { Archive::Entry::Entry(QObject *parent, QString fullPath, QString rootNode) : QObject(parent) , rootNode(rootNode) , compressedSizeIsSet(true) , m_parent(qobject_cast(parent)) , m_size(0) , m_compressedSize(0) , m_isDirectory(false) , m_isPasswordProtected(false) { if (!fullPath.isEmpty()) setFullPath(fullPath); } Archive::Entry::~Entry() { clear(); } +void Archive::Entry::copyMetaData(const Archive::Entry *sourceEntry) +{ + setProperty("fullPath", sourceEntry->property("fullPath")); + setProperty("permissions", sourceEntry->property("permissions")); + setProperty("owner", sourceEntry->property("owner")); + setProperty("group", sourceEntry->property("group")); + setProperty("size", sourceEntry->property("size")); + setProperty("compressedSize", sourceEntry->property("compressedSize")); + setProperty("link", sourceEntry->property("link")); + setProperty("ratio", sourceEntry->property("ratio")); + setProperty("CRC", sourceEntry->property("CRC")); + setProperty("method", sourceEntry->property("method")); + setProperty("version", sourceEntry->property("version")); + setProperty("timestamp", sourceEntry->property("timestamp").toDateTime()); + setProperty("isDirectory", sourceEntry->property("isDirectory")); + setProperty("comment", sourceEntry->property("comment")); + setProperty("isPasswordProtected", sourceEntry->property("isPasswordProtected")); +} + QVector Archive::Entry::entries() { Q_ASSERT(isDir()); return m_entries; } const QVector Archive::Entry::entries() const { Q_ASSERT(isDir()); return m_entries; } void Archive::Entry::setEntryAt(int index, Entry *value) { Q_ASSERT(isDir()); Q_ASSERT(index < m_entries.count()); m_entries[index] = value; } void Archive::Entry::appendEntry(Entry *entry) { Q_ASSERT(isDir()); m_entries.append(entry); } void Archive::Entry::removeEntryAt(int index) { Q_ASSERT(isDir()); Q_ASSERT(index < m_entries.count()); delete m_entries.takeAt(index); } Archive::Entry *Archive::Entry::getParent() const { return m_parent; } void Archive::Entry::setParent(Archive::Entry *parent) { m_parent = parent; } void Archive::Entry::setFullPath(const QString &fullPath) { m_fullPath = fullPath; m_fullPathWithoutTrailingSlash = fullPath; if (m_fullPathWithoutTrailingSlash.right(1) == QLatin1String("/")) { m_fullPathWithoutTrailingSlash.chop(1); } const QStringList pieces = m_fullPath.split(QLatin1Char( '/' ), QString::SkipEmptyParts); m_name = pieces.isEmpty() ? QString() : pieces.last(); } QString Archive::Entry::fullPath(bool withoutTrailingSlash) const { return (withoutTrailingSlash) ? m_fullPathWithoutTrailingSlash : m_fullPath; } QString Archive::Entry::name() const { return m_name; } void Archive::Entry::setIsDirectory(const bool isDirectory) { m_isDirectory = isDirectory; } bool Archive::Entry::isDir() const { return m_isDirectory; } int Archive::Entry::row() const { if (getParent()) { return getParent()->entries().indexOf(const_cast(this)); } return 0; } -Archive::Entry *Archive::Entry::find(const QString & name) +Archive::Entry *Archive::Entry::find(const QString &name) const { - foreach(Entry *entry, m_entries) { + foreach (Entry *entry, m_entries) { if (entry && (entry->name() == name)) { return entry; } } - return 0; + return Q_NULLPTR; } -Archive::Entry *Archive::Entry::findByPath(const QStringList & pieces, int index) +Archive::Entry *Archive::Entry::findByPath(const QStringList &pieces, int index) const { if (index == pieces.count()) { - return 0; + return Q_NULLPTR; } Entry *next = find(pieces.at(index)); - if (index == pieces.count() - 1) { return next; } if (next && next->isDir()) { return next->findByPath(pieces, index + 1); } - return 0; + return Q_NULLPTR; } void Archive::Entry::returnDirEntries(QList *store) { - foreach(Entry *entry, m_entries) { - if (entry->isDir()) { - store->prepend(entry); - entry->returnDirEntries(store); - } + foreach(Entry *entry, m_entries) { + if (entry->isDir()) { + store->prepend(entry); + entry->returnDirEntries(store); } + } } void Archive::Entry::clear() { if (isDir()) { qDeleteAll(m_entries); m_entries.clear(); } } bool Archive::Entry::operator==(const Archive::Entry &right) const { return m_fullPath == right.m_fullPath; } QDebug operator<<(QDebug d, const Kerfuffle::Archive::Entry &entry) { d.nospace() << "Entry(" << entry.property("fullPath"); if (!entry.rootNode.isEmpty()) { d.nospace() << "," << entry.rootNode; } d.nospace() << ")"; return d.space(); } QDebug operator<<(QDebug d, const Kerfuffle::Archive::Entry *entry) { d.nospace() << "Entry(" << entry->property("fullPath"); if (!entry->rootNode.isEmpty()) { d.nospace() << "," << entry->rootNode; } d.nospace() << ")"; return d.space(); } } diff --git a/kerfuffle/archiveentry.h b/kerfuffle/archiveentry.h index df444e36..0a973888 100644 --- a/kerfuffle/archiveentry.h +++ b/kerfuffle/archiveentry.h @@ -1,109 +1,111 @@ // // Created by mvlabat on 5/27/16. // #ifndef ARK_ENTRY_H #define ARK_ENTRY_H #include "archive_kerfuffle.h" #include "app/ark_debug.h" #include #include #include #include #include namespace Kerfuffle { class Archive::Entry : public QObject { Q_OBJECT /** * Meta data related to one entry in a compressed archive. * * When creating a plugin, information about every single entry in * an archive is contained in an ArchiveEntry, and metadata * is set with the entries in this enum. * * Please notice that not all archive formats support all the properties * below, so set those that are available. */ Q_PROPERTY(QString fullPath MEMBER m_fullPath WRITE setFullPath) Q_PROPERTY(QString name READ name) Q_PROPERTY(QString permissions MEMBER m_permissions) Q_PROPERTY(QString owner MEMBER m_owner) Q_PROPERTY(QString group MEMBER m_group) Q_PROPERTY(qulonglong size MEMBER m_size) Q_PROPERTY(qulonglong compressedSize MEMBER m_compressedSize) Q_PROPERTY(QString link MEMBER m_link) Q_PROPERTY(QString ratio MEMBER m_ratio) Q_PROPERTY(QString CRC MEMBER m_CRC) Q_PROPERTY(QString method MEMBER m_method) Q_PROPERTY(QString version MEMBER m_version) Q_PROPERTY(QDateTime timestamp MEMBER m_timestamp) Q_PROPERTY(bool isDirectory MEMBER m_isDirectory WRITE setIsDirectory) Q_PROPERTY(QString comment MEMBER m_comment) Q_PROPERTY(bool isPasswordProtected MEMBER m_isPasswordProtected) public: explicit Entry(QObject *parent = Q_NULLPTR, QString fullPath = QString(), QString rootNode = QString()); ~Entry(); + void copyMetaData(const Archive::Entry *sourceEntry); + QVector entries(); const QVector entries() const; void setEntryAt(int index, Entry *value); void appendEntry(Entry *entry); void removeEntryAt(int index); Entry *getParent() const; void setParent(Entry *parent); void setFullPath(const QString &fullPath); QString fullPath(bool withoutTrailingSlash = false) const; QString name() const; void setIsDirectory(const bool isDirectory); bool isDir() const; int row() const; - Entry *find(const QString & name); - Entry *findByPath(const QStringList & pieces, int index = 0); + Entry *find(const QString &name) const; + Entry *findByPath(const QStringList & pieces, int index = 0) const; void returnDirEntries(QList *store); void clear(); bool operator==(const Archive::Entry &right) const; public: QString rootNode; bool compressedSizeIsSet; private: QVector m_entries; QString m_name; Entry *m_parent; QString m_fullPath; QString m_fullPathWithoutTrailingSlash; QString m_permissions; QString m_owner; QString m_group; qulonglong m_size; qulonglong m_compressedSize; QString m_link; QString m_ratio; QString m_CRC; QString m_method; QString m_version; QDateTime m_timestamp; bool m_isDirectory; QString m_comment; bool m_isPasswordProtected; }; QDebug KERFUFFLE_EXPORT operator<<(QDebug d, const Kerfuffle::Archive::Entry &entry); QDebug KERFUFFLE_EXPORT operator<<(QDebug d, const Kerfuffle::Archive::Entry *entry); } Q_DECLARE_METATYPE(Kerfuffle::Archive::Entry*) #endif //ARK_ENTRY_H diff --git a/kerfuffle/archiveinterface.cpp b/kerfuffle/archiveinterface.cpp index c8f21d52..91e6d6d5 100644 --- a/kerfuffle/archiveinterface.cpp +++ b/kerfuffle/archiveinterface.cpp @@ -1,196 +1,196 @@ /* * 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 "archiveinterface.h" #include "ark_debug.h" #include #include #include #include namespace Kerfuffle { ReadOnlyArchiveInterface::ReadOnlyArchiveInterface(QObject *parent, const QVariantList & args) : QObject(parent) , m_waitForFinishedSignal(false) , m_isHeaderEncryptionEnabled(false) , m_isCorrupt(false) { qCDebug(ARK) << "Created read-only interface for" << args.first().toString(); m_filename = args.first().toString(); } ReadOnlyArchiveInterface::~ReadOnlyArchiveInterface() { } QString ReadOnlyArchiveInterface::filename() const { return m_filename; } QString ReadOnlyArchiveInterface::comment() const { return m_comment; } bool ReadOnlyArchiveInterface::isReadOnly() const { return true; } bool ReadOnlyArchiveInterface::open() { return true; } void ReadOnlyArchiveInterface::setPassword(const QString &password) { m_password = password; } void ReadOnlyArchiveInterface::setHeaderEncryptionEnabled(bool enabled) { m_isHeaderEncryptionEnabled = enabled; } QString ReadOnlyArchiveInterface::password() const { return m_password; } bool ReadOnlyArchiveInterface::doKill() { //default implementation return false; } bool ReadOnlyArchiveInterface::doSuspend() { //default implementation return false; } bool ReadOnlyArchiveInterface::doResume() { //default implementation return false; } void ReadOnlyArchiveInterface::setCorrupt(bool isCorrupt) { m_isCorrupt = isCorrupt; } bool ReadOnlyArchiveInterface::isCorrupt() const { return m_isCorrupt; } ReadWriteArchiveInterface::ReadWriteArchiveInterface(QObject *parent, const QVariantList & args) : ReadOnlyArchiveInterface(parent, args) { qCDebug(ARK) << "Created read-write interface for" << args.first().toString(); } ReadWriteArchiveInterface::~ReadWriteArchiveInterface() { } bool ReadOnlyArchiveInterface::waitForFinishedSignal() { return m_waitForFinishedSignal; } int ReadOnlyArchiveInterface::moveRequiredSignals() const { return 1; } int ReadOnlyArchiveInterface::copyRequiredSignals() const { return 1; } void ReadOnlyArchiveInterface::setWaitForFinishedSignal(bool value) { m_waitForFinishedSignal = value; } QStringList ReadOnlyArchiveInterface::entryFullPaths(const QList &entries, const bool withoutTrailingSlashes) { QStringList filesList; foreach (const Archive::Entry *file, entries) { filesList << file->fullPath(withoutTrailingSlashes); } return filesList; } -QList ReadOnlyArchiveInterface::entriesWithoutChildren(const QList &entries) +QList ReadOnlyArchiveInterface::entriesWithoutChildren(const QList &entries) { // QMap is easy way to get entries sorted by their fullPath. QMap sortedEntries; foreach (Archive::Entry *entry, entries) { sortedEntries.insert(entry->fullPath(), entry); } QList filteredEntries; QString lastFolder; foreach (Archive::Entry *entry, sortedEntries) { if (lastFolder.count() > 0 && entry->fullPath().startsWith(lastFolder)) continue; lastFolder = (entry->fullPath().right(1) == QLatin1String("/")) ? entry->fullPath() : QString(); filteredEntries << entry; } return filteredEntries; } bool ReadOnlyArchiveInterface::isHeaderEncryptionEnabled() const { return m_isHeaderEncryptionEnabled; } bool ReadWriteArchiveInterface::isReadOnly() const { // We set corrupt archives to read-only to avoid add/delete actions, that // are likely to fail anyway. if (isCorrupt()) { return true; } QFileInfo fileInfo(filename()); if (fileInfo.exists()) { return !fileInfo.isWritable(); } else { return !fileInfo.dir().exists(); // TODO: Should also check if we can create a file in that directory } } } // namespace Kerfuffle diff --git a/kerfuffle/cliinterface.cpp b/kerfuffle/cliinterface.cpp index e63d9ac3..0e5e8b0a 100644 --- a/kerfuffle/cliinterface.cpp +++ b/kerfuffle/cliinterface.cpp @@ -1,1442 +1,1495 @@ /* * 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; } int CliInterface::copyRequiredSignals() const { return 2; } 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::extractFiles(const QList &files, const QString &destinationDirectory, const ExtractionOptions &options) { qCDebug(ARK) << Q_FUNC_INFO << "to" << destinationDirectory; cacheParameterList(); m_operationMode = Extract; m_compressionOptions = options; m_extractedFiles = 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 = substituteExtractVariables(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 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 == Q_NULLPTR) ? QString() : destination->fullPath(); if (!destinationPath.isEmpty()) { m_extractTempDir = new QTemporaryDir(); const QString absoluteDestinationPath = m_extractTempDir->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->fullPath(true); const QString newFilePath = absoluteDestinationPath + file->fullPath(true); if (QFile::link(filePath, newFilePath)) { qCDebug(ARK) << "Symlink's created:" << filePath << newFilePath; } else { qCDebug(ARK) << "Can't create symlink" << filePath << newFilePath; delete m_extractTempDir; m_extractTempDir = Q_NULLPTR; return false; } } qCDebug(ARK) << "Changing working dir again to " << m_extractTempDir->path(); QDir::setCurrent(m_extractTempDir->path()); filesToPass.push_back(new Archive::Entry(preservedParent, destinationPath.split(QLatin1Char('/'), QString::SkipEmptyParts).at(0))); } else { filesToPass = files; } if (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, Archive::Entry *destination, const CompressionOptions &options) { cacheParameterList(); m_operationMode = Move; m_removedFiles = files; + QList withoutChildren = entriesWithoutChildren(files); + setNewMovedFiles(files, destination, withoutChildren.count()); const auto moveArgs = m_param.value(MoveArgs).toStringList(); const auto args = substituteMoveVariables(moveArgs, files, destination, password()); return runProcess(m_param.value(MoveProgram).toStringList(), args); } bool CliInterface::copyFiles(const QList &files, Archive::Entry *destination, const CompressionOptions &options) { m_oldWorkingDir = QDir::currentPath(); m_tempExtractDir = new QTemporaryDir(); m_tempAddDir = new QTemporaryDir(); QDir::setCurrent(m_tempExtractDir->path()); m_passedFiles = files; m_passedDestination = destination; m_passedOptions = options; m_passedOptions[QStringLiteral("PreservePaths")] = true; m_subOperation = Extract; connect(this, &CliInterface::finished, this, &CliInterface::continueCopying); return extractFiles(files, QDir::currentPath(), m_passedOptions); } 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 == Extract) { // Extraction jobs need a dedicated post-processing function. connect(m_process, static_cast(&KPtyProcess::finished), this, &CliInterface::extractProcessFinished, 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 || m_operationMode == Move) { QStringList removedFullPaths = entryFullPaths(m_removedFiles); foreach (const QString &fullPath, removedFullPaths) { emit entryRemoved(fullPath); } + foreach (Archive::Entry *e, m_newMovedFiles) { + emit entry(e); + } + m_newMovedFiles.clear(); } if (m_operationMode == Add) { if (m_extractTempDir) { delete m_extractTempDir; m_extractTempDir = Q_NULLPTR; } 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::extractProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) { Q_ASSERT(m_operationMode == Extract); m_exitCode = exitCode; qCDebug(ARK) << "Extraction process finished, exitcode:" << exitCode << "exitstatus:" << exitStatus; if (m_process) { // Handle all the remaining data in the process. readStdout(true); delete m_process; m_process = 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()); } cleanUpExtracting(); 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_extractedFiles.size())); cleanUpExtracting(); emit finished(false); return; } cleanUpExtracting(); } } if (m_compressionOptions.value(QStringLiteral("DragAndDrop")).toBool()) { if (!moveDroppedFilesToDest(m_extractedFiles, 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_extractedFiles.size())); cleanUpExtracting(); emit finished(false); return; } cleanUpExtracting(); } emit progress(1.0); emit finished(true); } void CliInterface::continueCopying(bool result) { if (!result) { finishCopying(false); return; } switch (m_subOperation) { case Extract: m_subOperation = Add; m_passedFiles = entriesWithoutChildren(m_passedFiles); if (!setAddedFiles() || !addFiles(m_tempAddedFiles, m_passedDestination, m_passedOptions)) { finishCopying(false); } break; case Add: finishCopying(true); break; default: Q_ASSERT(false); } } bool CliInterface::moveDroppedFilesToDest(const 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->fullPath().remove(file->rootNode)); QFileInfo absSourceEntry(QDir::current().absolutePath() + QLatin1Char('/') + file->fullPath()); QFileInfo absDestEntry(finalDestDir.path() + QLatin1Char('/') + relEntry.filePath()); if (absSourceEntry.isDir()) { // For directories, just create the path. if (!finalDestDir.mkpath(relEntry.filePath())) { qCWarning(ARK) << "Failed to create directory" << relEntry.filePath() << "in final destination."; } } else { // If destination file exists, prompt the user. if (absDestEntry.exists()) { qCWarning(ARK) << "File" << absDestEntry.absoluteFilePath() << "exists."; if (!skipAll && !overwriteAll) { Kerfuffle::OverwriteQuery query(absDestEntry.absoluteFilePath()); query.setNoRenameMode(true); 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::cleanUpExtracting() { if (!m_oldWorkingDir.isEmpty()) { QDir::setCurrent(m_oldWorkingDir); } if (m_extractTempDir) { delete m_extractTempDir; m_extractTempDir = Q_NULLPTR; } } void CliInterface::finishCopying(bool result) { disconnect(this, &CliInterface::finished, this, &CliInterface::continueCopying); emit progress(1.0); emit finished(result); cleanUp(); } bool CliInterface::moveToDestination(const QDir &tempDir, const QDir &destDir, bool preservePaths) { qCDebug(ARK) << "Moving extracted files from temp dir" << tempDir.path() << "to final destination" << destDir.path(); bool overwriteAll = false; bool skipAll = false; QDirIterator dirIt(tempDir.path(), QDir::AllEntries | QDir::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::substituteExtractVariables(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 << extractFilesList(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, true); 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::substituteMoveVariables(const QStringList &moveArgs, const QList &entries, const Archive::Entry *destination, const QString &password) +QStringList CliInterface::substituteMoveVariables(const QStringList &moveArgs, const QList &entriesWithoutChildren, const Archive::Entry *destination, const QString &password) { // Required if we call this function from unit tests. cacheParameterList(); QStringList args; foreach (const QString& arg, moveArgs) { qCDebug(ARK) << "Processing argument " << arg; if (arg == QLatin1String("$Archive")) { args << filename(); continue; } if (arg == QLatin1String("$PasswordSwitch")) { args << passwordSwitch(password); continue; } if (arg == QLatin1String("$PathPairs")) { - args << entryPathDestinationPairs(entriesWithoutChildren(entries), destination); + args << entryPathDestinationPairs(entriesWithoutChildren, destination); 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->fullPath(true)); } 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; } +void CliInterface::setNewMovedFiles(const QList &entries, const Archive::Entry *destination, int entriesWithoutChildren) +{ + m_newMovedFiles.clear(); + QMap entryMap; + foreach (const Archive::Entry* entry, entries) { + entryMap.insert(entry->fullPath(), entry); + } + + QString lastFolder; + + QString newPath; + int nameLength = 0; + foreach (const Archive::Entry* entry, entryMap) { + if (lastFolder.count() > 0 && entry->fullPath().startsWith(lastFolder)) { + // Replace last moved or copied folder path with destination path. + int charsCount = entry->fullPath().count() - lastFolder.count(); + if (entriesWithoutChildren > 1) { + charsCount += nameLength; + } + newPath = destination->fullPath() + entry->fullPath().right(charsCount); + } + else { + if (entriesWithoutChildren > 1) { + newPath = destination->fullPath() + entry->name(); + } + else { + // If there is only one passed file in the list, + // we have to use destination as newPath. + newPath = destination->fullPath(true); + } + if (entry->isDir()) { + newPath += QLatin1Char('/'); + nameLength = entry->name().count() + 1; // plus slash + lastFolder = entry->fullPath(); + } + else { + nameLength = 0; + lastFolder = QString(); + } + } + Archive::Entry *newEntry = new Archive::Entry(Q_NULLPTR); + newEntry->copyMetaData(entry); + newEntry->setFullPath(newPath); + m_newMovedFiles << newEntry; + } +} + 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::extractFilesList(const QList &entries) const { QStringList filesList; foreach (const Archive::Entry *e, entries) { filesList << escapeFileName(e->fullPath(true)); } 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::cleanUp() { qDeleteAll(m_tempAddedFiles); m_tempAddedFiles.clear(); QDir::setCurrent(m_oldWorkingDir); delete m_tempExtractDir; m_tempExtractDir = Q_NULLPTR; delete m_tempAddDir; m_tempAddDir = Q_NULLPTR; } 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)); } } } bool CliInterface::setAddedFiles() { QDir::setCurrent(m_tempAddDir->path()); foreach (const Archive::Entry *file, m_passedFiles) { const QString oldPath = m_tempExtractDir->path() + QLatin1Char('/') + file->fullPath(true); const QString newPath = m_tempAddDir->path() + QLatin1Char('/') + file->name(); if (!QFile::rename(oldPath, newPath)) { return false; } m_tempAddedFiles << new Archive::Entry(Q_NULLPTR, file->name()); } return true; } 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 == Extract || 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 == Extract) { 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; } -QStringList CliInterface::entryPathDestinationPairs(const QList &entries, const Archive::Entry *destination) +QStringList CliInterface::entryPathDestinationPairs(const QList &entriesWithoutChildren, const Archive::Entry *destination) { QStringList pairList; - if (entries.count() > 1) { - foreach (const Archive::Entry *file, entries) { + if (entriesWithoutChildren.count() > 1) { + foreach (const Archive::Entry *file, entriesWithoutChildren) { pairList << file->fullPath(true) << destination->fullPath() + file->name(); } } else { - pairList << entries.at(0)->fullPath(true) << destination->fullPath(true); + pairList << entriesWithoutChildren.at(0)->fullPath(true) << destination->fullPath(true); } return pairList; } void CliInterface::writeToProcess(const QByteArray& data) { Q_ASSERT(m_process); Q_ASSERT(!data.isNull()); qCDebug(ARK) << "Writing" << data << "to the process"; #ifdef Q_OS_WIN m_process->write(data); #else m_process->pty()->write(data); #endif } bool CliInterface::addComment(const QString &comment) { 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 f5145f32..2bcf89d5 100644 --- a/kerfuffle/cliinterface.h +++ b/kerfuffle/cliinterface.h @@ -1,496 +1,504 @@ /* * 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 "part/archivemodel.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, ///////////////[ MOVE ]///////////// /** * QStringList * The names to the program that will handle adding in this * archive format (eg "rar"). Will be searched for in PATH */ MoveProgram, /** * QStringList * The arguments that are passed to the program above for * moving inside the archive. Special strings that will be * substituted: * $Archive - the path of the archive * $Files - the files selected to be moved * $Destinations - new path of each file selected to be moved */ MoveArgs, ///////////////[ 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: OperationMode m_operationMode; explicit CliInterface(QObject *parent, const QVariantList & args); virtual ~CliInterface(); virtual int copyRequiredSignals() const; virtual bool list() Q_DECL_OVERRIDE; virtual bool extractFiles(const QList &files, const QString &destinationDirectory, const ExtractionOptions &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, Archive::Entry *destination, const CompressionOptions& options) Q_DECL_OVERRIDE; virtual bool copyFiles(const QList &files, 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 substituteExtractVariables(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 substituteMoveVariables(const QStringList &moveArgs, const QList &entries, const Archive::Entry *destination, const QString &password); + QStringList substituteMoveVariables(const QStringList &moveArgs, const QList &entriesWithoutChildren, const Archive::Entry *destination, const QString &password); QStringList substituteDeleteVariables(const QStringList &deleteArgs, const QList &entries, const QString &password); QStringList substituteCommentVariables(const QStringList &commentArgs, const QString &commentFile); QStringList substituteTestVariables(const QStringList &testArgs); + /** + * @see ArchiveModel::entryPathsFromDestination + */ + void setNewMovedFiles(const QList &entries, const Archive::Entry *destination, int entriesWithoutChildren); + /** * @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 extractFilesList(const QList &files) const; protected: bool setAddedFiles(); 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(); void cleanUp(); QString m_oldWorkingDir; ParameterList m_param; int m_exitCode; QTemporaryDir *m_tempExtractDir; QTemporaryDir *m_tempAddDir; OperationMode m_subOperation; QList m_passedFiles; QList m_tempAddedFiles; Archive::Entry *m_passedDestination; CompressionOptions m_passedOptions; 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; /** - * Constructs a list of path pairs which will be supplied to rn command. + * Returns a list of path pairs which will be supplied to rn command. * [ ... ] + * Also constructs a list of new entries resulted in moving. * - * @param entries List of archive entries + * @param entriesWithoutChildren List of archive entries * @param destination Must be a directory entry if QList contains more that one entry */ - virtual QStringList entryPathDestinationPairs(const QList &entries, const Archive::Entry *destination); + QStringList entryPathDestinationPairs(const QList &entriesWithoutChildren, const Archive::Entry *destination); /** * 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 cleanUpExtracting(); void finishCopying(bool result); QByteArray m_stdOutData; QRegularExpression m_passwordPromptPattern; QHash > m_patternCache; #ifdef Q_OS_WIN KProcess *m_process; #else KPtyProcess *m_process; #endif QList m_removedFiles; + QList m_newMovedFiles; bool m_listEmptyLines; bool m_abortingOperation; QString m_storedFileName; CompressionOptions m_compressionOptions; QString m_extractDestDir; QTemporaryDir *m_extractTempDir; QTemporaryFile *m_commentTempFile; QList m_extractedFiles; protected slots: virtual void processFinished(int exitCode, QProcess::ExitStatus exitStatus); private slots: void extractProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); void continueCopying(bool result); }; } #endif /* CLIINTERFACE_H */ diff --git a/part/CMakeLists.txt b/part/CMakeLists.txt index 1f17fb09..c222996e 100644 --- a/part/CMakeLists.txt +++ b/part/CMakeLists.txt @@ -1,38 +1,41 @@ include_directories(${CMAKE_SOURCE_DIR}/app) set(arkpart_PART_SRCS part.cpp infopanel.cpp arkviewer.cpp archivemodel.cpp archiveview.cpp jobtracker.cpp + overwritedialog.cpp ) ecm_qt_declare_logging_category(arkpart_PART_SRCS HEADER ark_debug.h IDENTIFIER ARK CATEGORY_NAME ark.part) qt5_add_dbus_adaptor(arkpart_PART_SRCS dnddbusinterface.xml part.h Ark::Part) ki18n_wrap_ui(arkpart_PART_SRCS arkviewer.ui infopanel.ui jobtracker.ui) add_library(arkpart MODULE ${arkpart_PART_SRCS}) target_link_libraries(arkpart kerfuffle KF5::Parts KF5::KIOFileWidgets) configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/ark_part.desktop.cmake ${CMAKE_CURRENT_BINARY_DIR}/ark_part.desktop ) +kcoreaddons_desktop_to_json(arkpart ${CMAKE_CURRENT_BINARY_DIR}/ark_part.desktop SERVICES_TYPES kpart.desktop browserview.desktop) + install(TARGETS arkpart DESTINATION ${KDE_INSTALL_PLUGINDIR}) ########### install files ############### install(FILES ${CMAKE_CURRENT_BINARY_DIR}/ark_part.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}) install(FILES ark_part.rc DESTINATION ${KDE_INSTALL_KXMLGUI5DIR}/ark) diff --git a/part/archivemodel.cpp b/part/archivemodel.cpp index fb81b001..a2e6cfa0 100644 --- a/part/archivemodel.cpp +++ b/part/archivemodel.cpp @@ -1,892 +1,1021 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 Harald Hvaal * Copyright (C) 2010-2012 Raphael Kubo da Costa * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "archivemodel.h" #include "kerfuffle/jobs.h" #include #include #include #include #include #include using namespace Kerfuffle; //used to speed up the loading of large archives static Archive::Entry *s_previousMatch = Q_NULLPTR; Q_GLOBAL_STATIC(QStringList, s_previousPieces) /** * Meta data related to one entry in a compressed archive. * * This is used for indexing entry properties as numbers * and for determining data displaying order in part's view. */ enum EntryMetaDataType { FullPath, /**< The entry's file name */ Size, /**< The entry's original size */ CompressedSize, /**< The compressed size for the entry */ Permissions, /**< The entry's permissions */ Owner, /**< The user the entry belongs to */ Group, /**< The user group the entry belongs to */ Ratio, /**< The compression ratio for the entry */ CRC, /**< The entry's CRC */ Method, /**< The compression method used on the entry */ Version, /**< The archiver version needed to extract the entry */ Timestamp, /**< The timestamp for the current entry */ Comment, }; /** * Mappings between column indexes and entry properties. */ static QMap initializePropertiesList() { QMap propertiesList = QMap(); propertiesList.insert(FullPath, QStringLiteral("fullPath")); propertiesList.insert(Size, QStringLiteral("size")); propertiesList.insert(CompressedSize, QStringLiteral("compressedSize")); propertiesList.insert(Permissions, QStringLiteral("permissions")); propertiesList.insert(Owner, QStringLiteral("owner")); propertiesList.insert(Group, QStringLiteral("group")); propertiesList.insert(Ratio, QStringLiteral("ratio")); propertiesList.insert(CRC, QStringLiteral("CRC")); propertiesList.insert(Method, QStringLiteral("method")); propertiesList.insert(Version, QStringLiteral("version")); propertiesList.insert(Timestamp, QStringLiteral("timestamp")); propertiesList.insert(Comment, QStringLiteral("comment")); return propertiesList; } static const QMap propertiesList = initializePropertiesList(); /** * Helper functor used by qStableSort. * * It always sorts folders before files. * * @internal */ class ArchiveModelSorter { public: ArchiveModelSorter(int column, Qt::SortOrder order) : m_sortColumn(column) , m_sortOrder(order) { } virtual ~ArchiveModelSorter() { } inline bool operator()(const QPair &left, const QPair &right) const { if (m_sortOrder == Qt::AscendingOrder) { return lessThan(left, right); } else { return !lessThan(left, right); } } protected: bool lessThan(const QPair &left, const QPair &right) const { const Archive::Entry * const leftEntry = left.first; const Archive::Entry * const rightEntry = right.first; // #234373: sort folders before files if ((leftEntry->isDir()) && (!rightEntry->isDir())) { return (m_sortOrder == Qt::AscendingOrder); } else if ((!leftEntry->isDir()) && (rightEntry->isDir())) { return !(m_sortOrder == Qt::AscendingOrder); } EntryMetaDataType column = static_cast(m_sortColumn); const QVariant &leftEntryMetaData = leftEntry->property(propertiesList[column].toUtf8()); const QVariant &rightEntryMetaData = rightEntry->property(propertiesList[column].toUtf8()); switch (m_sortColumn) { case FullPath: return leftEntry->name() < rightEntry->name(); case Size: case CompressedSize: return leftEntryMetaData.toInt() < rightEntryMetaData.toInt(); default: return leftEntryMetaData.toString() < rightEntryMetaData.toString(); } // We should not get here. Q_ASSERT(false); return false; } private: int m_sortColumn; Qt::SortOrder m_sortOrder; }; ArchiveModel::ArchiveModel(const QString &dbusPathName, QObject *parent) : QAbstractItemModel(parent) , m_rootEntry() , m_dbusPathName(dbusPathName) { m_rootEntry.setProperty("isDirectory", true); } ArchiveModel::~ArchiveModel() { } QVariant ArchiveModel::data(const QModelIndex &index, int role) const { if (index.isValid()) { Archive::Entry *entry = static_cast(index.internalPointer()); switch (role) { case Qt::DisplayRole: { //TODO: complete the columns int column = m_showColumns.at(index.column()); switch (column) { case FullPath: return entry->name(); case Size: if (entry->isDir()) { int dirs; int files; const int children = childCount(index, dirs, files); return KIO::itemsSummaryString(children, files, dirs, 0, false); } else if (!entry->property("link").toString().isEmpty()) { return QVariant(); } else { return KIO::convertSize(entry->property("size").toULongLong()); } case CompressedSize: if (entry->isDir() || !entry->property("link").toString().isEmpty()) { return QVariant(); } else { qulonglong compressedSize = entry->property("compressedSize").toULongLong(); if (compressedSize != 0) { return KIO::convertSize(compressedSize); } else { return QVariant(); } } case Ratio: // TODO: Use entry->metaData()[Ratio] when available if (entry->isDir() || !entry->property("link").toString().isEmpty()) { return QVariant(); } else { qulonglong compressedSize = entry->property("compressedSize").toULongLong(); qulonglong size = entry->property("size").toULongLong(); if (compressedSize == 0 || size == 0) { return QVariant(); } else { int ratio = int(100 * ((double)size - compressedSize) / size); return QString(QString::number(ratio) + QStringLiteral(" %")); } } case Timestamp: { const QDateTime timeStamp = entry->property("timestamp").toDateTime(); return QLocale().toString(timeStamp, QLocale::ShortFormat); } default: return entry->property(propertiesList[column].toUtf8()); } } case Qt::DecorationRole: if (index.column() == 0) { - return *m_entryIcons.value(entry->fullPath()); + return m_entryIcons.value(entry->fullPath(true)); } return QVariant(); case Qt::FontRole: { QFont f; f.setItalic(entry->property("isPasswordProtected").toBool()); return f; } default: return QVariant(); } } return QVariant(); } Qt::ItemFlags ArchiveModel::flags(const QModelIndex &index) const { Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index); if (index.isValid()) { return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | defaultFlags; } return 0; } QVariant ArchiveModel::headerData(int section, Qt::Orientation, int role) const { if (role == Qt::DisplayRole) { if (section >= m_showColumns.size()) { qCDebug(ARK) << "WEIRD: showColumns.size = " << m_showColumns.size() << " and section = " << section; return QVariant(); } int columnId = m_showColumns.at(section); switch (columnId) { case FullPath: return i18nc("Name of a file inside an archive", "Name"); case Size: return i18nc("Uncompressed size of a file inside an archive", "Size"); case CompressedSize: return i18nc("Compressed size of a file inside an archive", "Compressed"); case Ratio: return i18nc("Compression rate of file", "Rate"); case Owner: return i18nc("File's owner username", "Owner"); case Group: return i18nc("File's group", "Group"); case Permissions: return i18nc("File permissions", "Mode"); case CRC: return i18nc("CRC hash code", "CRC"); case Method: return i18nc("Compression method", "Method"); case Version: //TODO: what exactly is a file version? return i18nc("File version", "Version"); case Timestamp: return i18nc("Timestamp", "Date"); case Comment: return i18nc("File comment", "Comment"); default: return i18nc("Unnamed column", "??"); } } return QVariant(); } QModelIndex ArchiveModel::index(int row, int column, const QModelIndex &parent) const { if (hasIndex(row, column, parent)) { const Archive::Entry *parentEntry = parent.isValid() ? static_cast(parent.internalPointer()) : &m_rootEntry; Q_ASSERT(parentEntry->isDir()); const Archive::Entry *item = parentEntry->entries().value(row, Q_NULLPTR); if (item != Q_NULLPTR) { return createIndex(row, column, const_cast(item)); } } return QModelIndex(); } QModelIndex ArchiveModel::parent(const QModelIndex &index) const { if (index.isValid()) { Archive::Entry *item = static_cast(index.internalPointer()); Q_ASSERT(item); if (item->getParent() && (item->getParent() != &m_rootEntry)) { return createIndex(item->getParent()->row(), 0, item->getParent()); } } return QModelIndex(); } Archive::Entry *ArchiveModel::entryForIndex(const QModelIndex &index) { if (index.isValid()) { Archive::Entry *item = static_cast(index.internalPointer()); Q_ASSERT(item); return item; } return Q_NULLPTR; } int ArchiveModel::childCount(const QModelIndex &index, int &dirs, int &files) const { if (index.isValid()) { dirs = files = 0; Archive::Entry *item = static_cast(index.internalPointer()); Q_ASSERT(item); if (item->isDir()) { const QVector entries = item->entries(); foreach(const Archive::Entry *entry, entries) { if (entry->isDir()) { dirs++; } else { files++; } } return entries.count(); } return 0; } return -1; } int ArchiveModel::rowCount(const QModelIndex &parent) const { if (parent.column() <= 0) { const Archive::Entry *parentEntry = parent.isValid() ? static_cast(parent.internalPointer()) : &m_rootEntry; if (parentEntry && parentEntry->isDir()) { return parentEntry->entries().count(); } } return 0; } int ArchiveModel::columnCount(const QModelIndex &parent) const { return m_showColumns.size(); } void ArchiveModel::sort(int column, Qt::SortOrder order) { if (m_showColumns.size() <= column) { return; } emit layoutAboutToBeChanged(); QList dirEntries; m_rootEntry.returnDirEntries(&dirEntries); dirEntries.append(&m_rootEntry); const ArchiveModelSorter modelSorter(m_showColumns.at(column), order); foreach(Archive::Entry *dir, dirEntries) { QVector < QPair > sorting(dir->entries().count()); for (int i = 0; i < dir->entries().count(); ++i) { Archive::Entry *item = dir->entries().at(i); sorting[i].first = item; sorting[i].second = i; } qStableSort(sorting.begin(), sorting.end(), modelSorter); QModelIndexList fromIndexes; QModelIndexList toIndexes; for (int r = 0; r < sorting.count(); ++r) { Archive::Entry *item = sorting.at(r).first; toIndexes.append(createIndex(r, 0, item)); fromIndexes.append(createIndex(sorting.at(r).second, 0, sorting.at(r).first)); dir->setEntryAt(r, sorting.at(r).first); } changePersistentIndexList(fromIndexes, toIndexes); emit dataChanged( index(0, 0, indexForEntry(dir)), index(dir->entries().size() - 1, 0, indexForEntry(dir))); } emit layoutChanged(); } Qt::DropActions ArchiveModel::supportedDropActions() const { return Qt::CopyAction | Qt::MoveAction; } QStringList ArchiveModel::mimeTypes() const { QStringList types; // MIME types we accept for dragging (eg. Dolphin -> Ark). types << QStringLiteral("text/uri-list") << QStringLiteral("text/plain") << QStringLiteral("text/x-moz-url"); // MIME types we accept for dropping (eg. Ark -> Dolphin). types << QStringLiteral("application/x-kde-ark-dndextract-service") << QStringLiteral("application/x-kde-ark-dndextract-path"); return types; } QMimeData *ArchiveModel::mimeData(const QModelIndexList &indexes) const { Q_UNUSED(indexes) QMimeData *mimeData = new QMimeData; mimeData->setData(QStringLiteral("application/x-kde-ark-dndextract-service"), QDBusConnection::sessionBus().baseService().toUtf8()); mimeData->setData(QStringLiteral("application/x-kde-ark-dndextract-path"), m_dbusPathName.toUtf8()); return mimeData; } bool ArchiveModel::dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) { Q_UNUSED(action) - Q_UNUSED(row) - Q_UNUSED(column) - Q_UNUSED(parent) if (!data->hasUrls()) { return false; } QStringList paths; foreach(const QUrl &url, data->urls()) { paths << url.toLocalFile(); } - //for now, this code is not used because adding files to paths inside the - //archive is not supported yet. need a solution for this later. - QString path; -#if 0 - if (parent.isValid()) { - QModelIndex droppedOnto = index(row, column, parent); - Archive::Entry *entry = entryForIndex(droppedOnto); - if (entry->isDir()) { - qCDebug(ARK) << "Using entry"; - path = entry->fileName.toString(); - } else { - path = entryForIndex(parent)->fileName.toString(); + const Archive::Entry *entry = Q_NULLPTR; + QModelIndex droppedOnto = index(row, column, parent); + if (droppedOnto.isValid()) { + entry = entryForIndex(droppedOnto); + if (!entry->isDir()) { + entry = entry->getParent(); } } - qCDebug(ARK) << "Dropped onto " << path; - -#endif - - emit droppedFiles(paths, path); + emit droppedFiles(paths, entry, QString()); return true; } // For a rationale, see bugs #194241, #241967 and #355839 QString ArchiveModel::cleanFileName(const QString& fileName) { // Skip entries with filename "/" or "//" or "." // "." is present in ISO files QRegularExpression pattern(QStringLiteral("/+|\\.")); QRegularExpressionMatch match; if (fileName.contains(pattern, &match) && match.captured() == fileName) { qCDebug(ARK) << "Skipping entry with filename" << fileName; return QString(); } else if (fileName.startsWith(QLatin1String("./"))) { return fileName.mid(2); } return fileName; } Archive::Entry *ArchiveModel::parentFor(const Archive::Entry *entry) { QStringList pieces = entry->fullPath().split(QLatin1Char( '/' ), QString::SkipEmptyParts); if (pieces.isEmpty()) { return Q_NULLPTR; } pieces.removeLast(); if (s_previousMatch) { //the number of path elements must be the same for the shortcut //to work if (s_previousPieces->count() == pieces.count()) { bool equal = true; //make sure all the pieces match up for (int i = 0; i < s_previousPieces->count(); ++i) { if (s_previousPieces->at(i) != pieces.at(i)) { equal = false; break; } } //if match return it if (equal) { return s_previousMatch; } } } Archive::Entry *parent = &m_rootEntry; foreach(const QString &piece, pieces) { Archive::Entry *entry = parent->find(piece); if (!entry) { // Directory entry will be traversed later (that happens for some archive formats, 7z for instance). // We have to create one before, in order to construct tree from its children, // and then delete the existing one (see ArchiveModel::newEntry). entry = new Archive::Entry(parent); entry->setProperty("fullPath", (parent == &m_rootEntry) ? piece : parent->fullPath() + QLatin1Char( '/' ) + piece); entry->setProperty("isDirectory", true); insertEntry(entry); } if (!entry->isDir()) { Archive::Entry *e = new Archive::Entry(parent); - copyEntryMetaData(e, entry); + e->copyMetaData(entry); // Maybe we have both a file and a directory of the same name. // We avoid removing previous entries unless necessary. insertEntry(e); } parent = entry; } s_previousMatch = parent; *s_previousPieces = pieces; return parent; } QModelIndex ArchiveModel::indexForEntry(Archive::Entry *entry) { Q_ASSERT(entry); if (entry != &m_rootEntry) { Q_ASSERT(entry->getParent()); Q_ASSERT(entry->getParent()->isDir()); return createIndex(entry->row(), 0, entry); } return QModelIndex(); } void ArchiveModel::slotEntryRemoved(const QString & path) { const QString entryFileName(cleanFileName(path)); if (entryFileName.isEmpty()) { return; } Archive::Entry *entry = m_rootEntry.findByPath(entryFileName.split(QLatin1Char( '/' ), QString::SkipEmptyParts)); if (entry) { Archive::Entry *parent = entry->getParent(); QModelIndex index = indexForEntry(entry); Q_UNUSED(index); beginRemoveRows(indexForEntry(parent), entry->row(), entry->row()); - delete m_entryIcons.take(parent->entries().at(entry->row())->fullPath()); + m_entryIcons.remove(parent->entries().at(entry->row())->fullPath(true)); parent->removeEntryAt(entry->row()); endRemoveRows(); } } void ArchiveModel::slotUserQuery(Kerfuffle::Query *query) { query->execute(); } void ArchiveModel::slotNewEntryFromSetArchive(Archive::Entry *entry) { // we cache all entries that appear when opening a new archive // so we can all them together once it's done, this is a huge // performance improvement because we save from doing lots of // begin/endInsertRows m_newArchiveEntries.push_back(entry); } void ArchiveModel::slotNewEntry(Archive::Entry *entry) { newEntry(entry, NotifyViews); } void ArchiveModel::newEntry(Archive::Entry *receivedEntry, InsertBehaviour behaviour) { if (receivedEntry->fullPath().isEmpty()) { qCDebug(ARK) << "Weird, received empty entry (no filename) - skipping"; return; } //if there are no addidional columns registered, then have a look at the //entry and populate some if (m_showColumns.isEmpty()) { QList toInsert; QMap::const_iterator i = propertiesList.begin(); while (i != propertiesList.end()) { if (!receivedEntry->property(i.value().toUtf8()).toString().isEmpty()) { if (i.key() != CompressedSize || receivedEntry->compressedSizeIsSet) { toInsert << i.key(); } } ++i; } beginInsertColumns(QModelIndex(), 0, toInsert.size() - 1); m_showColumns << toInsert; endInsertColumns(); qCDebug(ARK) << "Showing columns: " << m_showColumns; } //#194241: Filenames such as "./file" should be displayed as "file" //#241967: Entries called "/" should be ignored //#355839: Entries called "//" should be ignored QString entryFileName = cleanFileName(receivedEntry->fullPath()); if (entryFileName.isEmpty()) { // The entry contains only "." or "./" return; } receivedEntry->setProperty("fullPath", entryFileName); /// 1. Skip already created entries Archive::Entry *existing = m_rootEntry.findByPath(entryFileName.split(QLatin1Char( '/' ))); if (existing) { qCDebug(ARK) << "Refreshing entry for" << entryFileName; existing->setProperty("fullPath", entryFileName); // Multi-volume files are repeated at least in RAR archives. // In that case, we need to sum the compressed size for each volume qulonglong currentCompressedSize = existing->property("compressedSize").toULongLong(); existing->setProperty("compressedSize", currentCompressedSize + receivedEntry->property("compressedSize").toULongLong()); return; } /// 2. Find Parent Entry, creating missing direcotry ArchiveEntries in the process Archive::Entry *parent = parentFor(receivedEntry); /// 3. Create an Archive::Entry const QStringList path = entryFileName.split(QLatin1Char('/'), QString::SkipEmptyParts); const QString name = path.last(); Archive::Entry *entry = parent->find(name); if (entry) { - copyEntryMetaData(entry, receivedEntry); + entry->copyMetaData(receivedEntry); entry->setProperty("fullPath", entryFileName); delete receivedEntry; } else { receivedEntry->setParent(parent); insertEntry(receivedEntry, behaviour); } } void ArchiveModel::slotLoadingFinished(KJob *job) { int i = 0; foreach(Archive::Entry *entry, m_newArchiveEntries) { newEntry(entry, DoNotNotifyViews); i++; } beginResetModel(); endResetModel(); m_newArchiveEntries.clear(); qCDebug(ARK) << "Added" << i << "entries to model"; emit loadingFinished(job); } -void ArchiveModel::copyEntryMetaData(Archive::Entry *destinationEntry, const Archive::Entry *sourceEntry) -{ - destinationEntry->setProperty("fullPath", sourceEntry->property("fullPath")); - destinationEntry->setProperty("permissions", sourceEntry->property("permissions")); - destinationEntry->setProperty("owner", sourceEntry->property("owner")); - destinationEntry->setProperty("group", sourceEntry->property("group")); - destinationEntry->setProperty("size", sourceEntry->property("size")); - destinationEntry->setProperty("compressedSize", sourceEntry->property("compressedSize")); - destinationEntry->setProperty("link", sourceEntry->property("link")); - destinationEntry->setProperty("ratio", sourceEntry->property("ratio")); - destinationEntry->setProperty("CRC", sourceEntry->property("CRC")); - destinationEntry->setProperty("method", sourceEntry->property("method")); - destinationEntry->setProperty("version", sourceEntry->property("version")); - destinationEntry->setProperty("timestamp", sourceEntry->property("timestamp").toDateTime()); - destinationEntry->setProperty("isDirectory", sourceEntry->property("isDirectory")); - destinationEntry->setProperty("comment", sourceEntry->property("comment")); - destinationEntry->setProperty("isPasswordProtected", sourceEntry->property("isPasswordProtected")); -} - void ArchiveModel::insertEntry(Archive::Entry *entry, InsertBehaviour behaviour) { Q_ASSERT(entry); Archive::Entry *parent = entry->getParent(); Q_ASSERT(parent); if (behaviour == NotifyViews) { beginInsertRows(indexForEntry(parent), parent->entries().count(), parent->entries().count()); } parent->appendEntry(entry); if (behaviour == NotifyViews) { endInsertRows(); } // Save an icon for each newly added entry. QMimeDatabase db; - const QPixmap *pixmap; + QPixmap pixmap; if (entry->isDir()) { - pixmap = new QPixmap(QIcon::fromTheme(db.mimeTypeForName(QStringLiteral("inode/directory")).iconName()).pixmap(IconSize(KIconLoader::Small), + pixmap = QPixmap(QIcon::fromTheme(db.mimeTypeForName(QStringLiteral("inode/directory")).iconName()).pixmap(IconSize(KIconLoader::Small), IconSize(KIconLoader::Small))); } else { - pixmap = new QPixmap(QIcon::fromTheme(db.mimeTypeForFile(entry->fullPath()).iconName()).pixmap(IconSize(KIconLoader::Small), + pixmap = QPixmap(QIcon::fromTheme(db.mimeTypeForFile(entry->fullPath()).iconName()).pixmap(IconSize(KIconLoader::Small), IconSize(KIconLoader::Small))); } - m_entryIcons.insert(entry->fullPath(), pixmap); + m_entryIcons.insert(entry->fullPath(true), pixmap); } Kerfuffle::Archive* ArchiveModel::archive() const { return m_archive.data(); } KJob* ArchiveModel::setArchive(Kerfuffle::Archive *archive) { m_archive.reset(archive); m_rootEntry.clear(); s_previousMatch = Q_NULLPTR; s_previousPieces->clear(); Kerfuffle::ListJob *job = Q_NULLPTR; m_newArchiveEntries.clear(); if (m_archive) { job = m_archive->list(); // TODO: call "open" or "create"? if (job) { connect(job, &Kerfuffle::ListJob::newEntry, this, &ArchiveModel::slotNewEntryFromSetArchive); connect(job, &Kerfuffle::ListJob::result, this, &ArchiveModel::slotLoadingFinished); connect(job, &Kerfuffle::ListJob::userQuery, this, &ArchiveModel::slotUserQuery); emit loadingStarted(); // TODO: make sure if it's ok to not have calls to beginRemoveColumns here m_showColumns.clear(); } } beginResetModel(); endResetModel(); return job; } ExtractJob* ArchiveModel::extractFile(Archive::Entry *file, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options) const { QList files; files << file; return extractFiles(files, destinationDir, options); } ExtractJob* ArchiveModel::extractFiles(const QList& files, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options) const { Q_ASSERT(m_archive); ExtractJob *newJob = m_archive->extractFiles(files, destinationDir, options); connect(newJob, &ExtractJob::userQuery, this, &ArchiveModel::slotUserQuery); return newJob; } Kerfuffle::PreviewJob *ArchiveModel::preview(Archive::Entry *file) const { Q_ASSERT(m_archive); PreviewJob *job = m_archive->preview(file); connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery); return job; } OpenJob *ArchiveModel::open(Archive::Entry *file) const { Q_ASSERT(m_archive); OpenJob *job = m_archive->open(file); connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery); return job; } OpenWithJob *ArchiveModel::openWith(Archive::Entry *file) const { Q_ASSERT(m_archive); OpenWithJob *job = m_archive->openWith(file); connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery); return job; } AddJob* ArchiveModel::addFiles(QList &entries, const Archive::Entry *destination, const CompressionOptions& options) { if (!m_archive) { return Q_NULLPTR; } if (!m_archive->isReadOnly()) { AddJob *job = m_archive->addFiles(entries, destination, options); connect(job, &AddJob::newEntry, this, &ArchiveModel::slotNewEntry); connect(job, &AddJob::userQuery, this, &ArchiveModel::slotUserQuery); return job; } return Q_NULLPTR; } +Kerfuffle::MoveJob *ArchiveModel::moveFiles(QList &entries, Archive::Entry *destination, const CompressionOptions &options) +{ + if (!m_archive) { + return Q_NULLPTR; + } + + if (!m_archive->isReadOnly()) { + MoveJob *job = m_archive->moveFiles(entries, destination, options); + connect(job, &MoveJob::newEntry, this, &ArchiveModel::slotNewEntry); + connect(job, &MoveJob::userQuery, this, &ArchiveModel::slotUserQuery); + connect(job, &MoveJob::entryRemoved, this, &ArchiveModel::slotEntryRemoved); + connect(job, &MoveJob::finished, this, &ArchiveModel::slotCleanupEmptyDirs); + + + return job; + } + return Q_NULLPTR; +} +Kerfuffle::CopyJob *ArchiveModel::copyFiles(QList &entries, Archive::Entry *destination, const CompressionOptions &options) +{ + if (!m_archive) { + return Q_NULLPTR; + } + + if (!m_archive->isReadOnly()) { + CopyJob *job = m_archive->copyFiles(entries, destination, options); + connect(job, &CopyJob::newEntry, this, &ArchiveModel::slotNewEntry); + connect(job, &CopyJob::userQuery, this, &ArchiveModel::slotUserQuery); + + + return job; + } + return Q_NULLPTR; +} + DeleteJob* ArchiveModel::deleteFiles(QList entries) { Q_ASSERT(m_archive); if (!m_archive->isReadOnly()) { DeleteJob *job = m_archive->deleteFiles(entries); connect(job, &DeleteJob::entryRemoved, this, &ArchiveModel::slotEntryRemoved); connect(job, &DeleteJob::finished, this, &ArchiveModel::slotCleanupEmptyDirs); connect(job, &DeleteJob::userQuery, this, &ArchiveModel::slotUserQuery); return job; } return Q_NULLPTR; } void ArchiveModel::encryptArchive(const QString &password, bool encryptHeader) { if (!m_archive) { return; } m_archive->encrypt(password, encryptHeader); } +QStringList ArchiveModel::entryPathsFromDestination(QStringList entries, const Archive::Entry *destination, int entriesWithoutChildren) +{ + QStringList paths = QStringList(); + entries.sort(); + QString lastFolder; + const QString destinationPath = (destination == Q_NULLPTR) ? QString() : destination->fullPath(); + + QString newPath; + int nameLength = 0; + foreach (const QString &entryPath, entries) { + if (lastFolder.count() > 0 && entryPath.startsWith(lastFolder)) { + // Replace last moved or copied folder path with destination path. + int charsCount = entryPath.count() - lastFolder.count(); + if (entriesWithoutChildren != 1) { + charsCount += nameLength; + } + newPath = destinationPath + entryPath.right(charsCount); + } + else { + const QString name = entryPath.split(QLatin1Char('/'), QString::SkipEmptyParts).last(); + if (entriesWithoutChildren != 1) { + newPath = destinationPath + name; + if (entryPath.right(1) == QLatin1String("/")) { + newPath += QLatin1Char('/'); + } + } + else { + // If the mode is set to Move and there is only one passed file in the list, + // we have to use destination as newPath. + newPath = destinationPath; + } + if (entryPath.right(1) == QLatin1String("/")) { + nameLength = name.count() + 1; // plus slash + lastFolder = entryPath; + } + else { + nameLength = 0; + lastFolder = QString(); + } + } + paths << newPath; + } + + return paths; +} + +bool ArchiveModel::conflictingEntries(QList &conflictingEntries, const QStringList &entries, bool allowMerging) const +{ + bool error = false; + + // We can't accept destination as an argument, because it can be a new entry path for renaming. + const Archive::Entry *destination; + { + QStringList destinationParts = entries.first().split(QLatin1Char('/'), QString::SkipEmptyParts); + destinationParts.removeLast(); + if (destinationParts.count() > 0) { + destination = m_rootEntry.findByPath(destinationParts); + } + else { + destination = &m_rootEntry; + } + } + const Archive::Entry *lastDirEntry = destination; + QString skippedDirPath; + + foreach (const QString &entry, entries) { + if (skippedDirPath.count() > 0 && entry.startsWith(skippedDirPath)) { + continue; + } + else { + skippedDirPath.clear(); + } + + while (!entry.startsWith(lastDirEntry->fullPath())) { + lastDirEntry = lastDirEntry->getParent(); + } + + bool isDir = entry.right(1) == QLatin1String("/"); + const Archive::Entry *archiveEntry = lastDirEntry->find(entry.split(QLatin1Char('/'), QString::SkipEmptyParts).last()); + + if (archiveEntry != Q_NULLPTR) { + if (archiveEntry->isDir() != isDir || !allowMerging) { + if (isDir) { + skippedDirPath = lastDirEntry->fullPath(); + } + + if (!error) { + conflictingEntries.clear(); + error = true; + } + conflictingEntries << archiveEntry; + } + else { + if (isDir) { + lastDirEntry = archiveEntry; + } + else if (!error) { + conflictingEntries << archiveEntry; + } + } + } + else if (isDir) { + skippedDirPath = entry; + } + } + + return error; +} + +bool ArchiveModel::hasDuplicatedEntries(const QStringList &entries) +{ + QStringList tempList; + foreach (const QString &entry, entries) { + if (tempList.contains(entry)) { + return true; + } + tempList << entry; + } + return false; +} + +const QHash ArchiveModel::entryIcons() const +{ + return m_entryIcons; +} + void ArchiveModel::slotCleanupEmptyDirs() { QList queue; QList nodesToDelete; //add root nodes for (int i = 0; i < rowCount(); ++i) { queue.append(QPersistentModelIndex(index(i, 0))); } //breadth-first traverse while (!queue.isEmpty()) { QPersistentModelIndex node = queue.takeFirst(); Archive::Entry *entry = entryForIndex(node); if (!hasChildren(node)) { if (entry->fullPath().isEmpty()) { nodesToDelete << node; } } else { for (int i = 0; i < rowCount(node); ++i) { queue.append(QPersistentModelIndex(index(i, 0, node))); } } } foreach(const QPersistentModelIndex& node, nodesToDelete) { Archive::Entry *rawEntry = static_cast(node.internalPointer()); qCDebug(ARK) << "Delete with parent entries " << rawEntry->getParent()->entries() << " and row " << rawEntry->row(); beginRemoveRows(parent(node), rawEntry->row(), rawEntry->row()); - delete m_entryIcons.take(rawEntry->getParent()->entries().at(rawEntry->row())->fullPath()); + m_entryIcons.remove(rawEntry->getParent()->entries().at(rawEntry->row())->fullPath(true)); rawEntry->getParent()->removeEntryAt(rawEntry->row()); endRemoveRows(); } } diff --git a/part/archivemodel.h b/part/archivemodel.h index 39b03e09..4bd145e0 100644 --- a/part/archivemodel.h +++ b/part/archivemodel.h @@ -1,134 +1,178 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 Harald Hvaal * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #ifndef ARCHIVEMODEL_H #define ARCHIVEMODEL_H #include #include #include #include "kerfuffle/archiveentry.h" using Kerfuffle::Archive; namespace Kerfuffle { class Query; } class ArchiveModel: public QAbstractItemModel { Q_OBJECT public: explicit ArchiveModel(const QString &dbusPathName, QObject *parent = 0); ~ArchiveModel(); QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE; Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; QModelIndex parent(const QModelIndex &index) const Q_DECL_OVERRIDE; int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; virtual void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) Q_DECL_OVERRIDE; //drag and drop related Qt::DropActions supportedDropActions() const Q_DECL_OVERRIDE; QStringList mimeTypes() const Q_DECL_OVERRIDE; QMimeData *mimeData(const QModelIndexList & indexes) const Q_DECL_OVERRIDE; bool dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) Q_DECL_OVERRIDE; KJob* setArchive(Kerfuffle::Archive *archive); Kerfuffle::Archive *archive() const; Archive::Entry *entryForIndex(const QModelIndex &index); int childCount(const QModelIndex &index, int &dirs, int &files) const; Kerfuffle::ExtractJob* extractFile(Archive::Entry *file, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options = Kerfuffle::ExtractionOptions()) const; Kerfuffle::ExtractJob* extractFiles(const QList& files, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options = Kerfuffle::ExtractionOptions()) const; Kerfuffle::PreviewJob* preview(Archive::Entry *file) const; Kerfuffle::OpenJob* open(Archive::Entry *file) const; Kerfuffle::OpenWithJob* openWith(Archive::Entry *file) const; Kerfuffle::AddJob* addFiles(QList &entries, const Archive::Entry *destination, const Kerfuffle::CompressionOptions& options = Kerfuffle::CompressionOptions()); + Kerfuffle::MoveJob* moveFiles(QList &entries, Archive::Entry *destination, const Kerfuffle::CompressionOptions& options = Kerfuffle::CompressionOptions()); + Kerfuffle::CopyJob* copyFiles(QList &entries, Archive::Entry *destination, const Kerfuffle::CompressionOptions& options = Kerfuffle::CompressionOptions()); Kerfuffle::DeleteJob* deleteFiles(QList entries); /** * @param password The password to encrypt the archive with. * @param encryptHeader Whether to encrypt also the list of files. */ void encryptArchive(const QString &password, bool encryptHeader); + /** + * Returns the string list of entry paths, which will be a result of adding/moving/copying entries. + * + * @param entries The entries which will be added/moved/copied. + * @param destination Destination path within the archive to which entries have to be added. For renaming an entry + * the path has to contain a new filename too. + * @param entriesWithoutChildren Entries count, excluding their children. For AddJob or CopyJob 0 MUST be passed. + * + * @return For entries + * some/dir/ + * some/dir/entry + * some/dir/some/entry + * some/another/entry + * and destination + * some/destination + * will return + * some/destination/dir/ + * some/destination/dir/entry + * some/destination/dir/some/enty + * some/destination/entry + */ + static QStringList entryPathsFromDestination(QStringList entries, const Archive::Entry *destination, int entriesWithoutChildren); + + /** + * Constructs a list of conflicting entries. + * + * @param conflictingEntries Reference to the empty mutable entries list, which will be constructed. + * If the method returns false, this list will contain only entries which produce a critical conflict. + * @param entries New entries paths list. + * @param allowMerging Boolean variable indicating whether merging is permitted. + * If true, existing entries won't generate an error. + * + * @return Boolean variable indicating whether conflicts are not critical (true for not critical, + * false for critical). For example, if there are both "some/file" (not a directory) and "some/file/" (a directory) + * entries for both new and existing paths, the method will return false. Also, if merging is not allowed, + * this method will return false for entries with the same path and types. + */ + bool conflictingEntries(QList &conflictingEntries, const QStringList &entries, bool allowMerging) const; + + static bool hasDuplicatedEntries(const QStringList &entries); + + const QHash entryIcons() const; + signals: void loadingStarted(); void loadingFinished(KJob *); void extractionFinished(bool success); void error(const QString& error, const QString& details); - void droppedFiles(const QStringList& files, const QString& path = QString()); + void droppedFiles(const QStringList& files, const Archive::Entry*, const QString&); private slots: void slotNewEntryFromSetArchive(Archive::Entry *entry); void slotNewEntry(Archive::Entry *entry); void slotLoadingFinished(KJob *job); void slotEntryRemoved(const QString & path); void slotUserQuery(Kerfuffle::Query *query); void slotCleanupEmptyDirs(); private: /** * Strips file names that start with './'. * * For more information, see bug 194241. * * @param fileName The file name that will be stripped. * * @return @p fileName without the leading './' */ QString cleanFileName(const QString& fileName); Archive::Entry *parentFor(const Kerfuffle::Archive::Entry *entry); QModelIndex indexForEntry(Archive::Entry *entry); static bool compareAscending(const QModelIndex& a, const QModelIndex& b); static bool compareDescending(const QModelIndex& a, const QModelIndex& b); /** * Insert the node @p node into the model, ensuring all views are notified * of the change. */ enum InsertBehaviour { NotifyViews, DoNotNotifyViews }; - void copyEntryMetaData(Archive::Entry *destinationEntry, const Archive::Entry *sourceEntry); void insertEntry(Archive::Entry *entry, InsertBehaviour behaviour = NotifyViews); void newEntry(Kerfuffle::Archive::Entry *receivedEntry, InsertBehaviour behaviour); QList m_newArchiveEntries; // holds entries from opening a new archive until it's totally open QList m_showColumns; QScopedPointer m_archive; Archive::Entry m_rootEntry; - QHash m_entryIcons; + QHash m_entryIcons; QString m_dbusPathName; }; #endif // ARCHIVEMODEL_H diff --git a/part/archiveview.cpp b/part/archiveview.cpp index 7a31a863..1eca8b7a 100644 --- a/part/archiveview.cpp +++ b/part/archiveview.cpp @@ -1,105 +1,167 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2008-2009 Harald Hvaal * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "archiveview.h" #include "ark_debug.h" #include #include #include #include #include #include +#include ArchiveView::ArchiveView(QWidget *parent) : QTreeView(parent) { } void ArchiveView::setModel(QAbstractItemModel *model) { QTreeView::setModel(model); setSelectionMode(QAbstractItemView::ExtendedSelection); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); setAlternatingRowColors(true); setAnimated(true); setAllColumnsShowFocus(true); setSortingEnabled(true); //drag and drop setDragEnabled(true); setAcceptDrops(true); setDropIndicatorShown(true); setDragDropMode(QAbstractItemView::DragDrop); } void ArchiveView::startDrag(Qt::DropActions supportedActions) { //only start the drag if it's over the filename column. this allows dragging selection in //tree/detail view if (currentIndex().column() != 0) { return; } QTreeView::startDrag(supportedActions); } void ArchiveView::dragEnterEvent(QDragEnterEvent * event) { //TODO: if no model, trigger some mechanism to create one automatically! qCDebug(ARK) << "dragEnterEvent" << event; if (event->source() == this) { //we don't support internal drops yet. return; } QTreeView::dragEnterEvent(event); } void ArchiveView::dropEvent(QDropEvent * event) { qCDebug(ARK) << "dropEvent" << event; if (event->source() == this) { //we don't support internal drops yet. return; } QTreeView::dropEvent(event); } void ArchiveView::dragMoveEvent(QDragMoveEvent * event) { qCDebug(ARK) << "dragMoveEvent" << event; if (event->source() == this) { //we don't support internal drops yet. return; } QTreeView::dragMoveEvent(event); if (event->mimeData()->hasFormat(QStringLiteral("text/uri-list"))) { event->acceptProposedAction(); } } + +bool ArchiveView::eventFilter(QObject *object, QEvent *event) +{ + if (object == m_entryEditor && event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Escape) { + closeEntryEditor(); + return true; + } + } + return false; +} + +void ArchiveView::mouseReleaseEvent(QMouseEvent *event) +{ + if (m_editorIndex.isValid()) { + closeEntryEditor(); + } + else { + QTreeView::mouseReleaseEvent(event); + } +} + +void ArchiveView::keyPressEvent(QKeyEvent *event) +{ + if (m_editorIndex.isValid()) { + switch (event->key()) { + case Qt::Key_Return: + case Qt::Key_Enter: { + QLineEdit* editor = static_cast(indexWidget(m_editorIndex)); + emit entryChanged(editor->text()); + closeEntryEditor(); + break; + } + + default: + QTreeView::keyPressEvent(event); + } + } + else { + QTreeView::keyPressEvent(event); + } +} + +void ArchiveView::openEntryEditor(QModelIndex index) +{ + m_editorIndex = index; + openPersistentEditor(index); + m_entryEditor = static_cast(indexWidget(m_editorIndex)); + m_entryEditor->installEventFilter(this); + m_entryEditor->setText(index.data().toString()); + m_entryEditor->setFocus(Qt::OtherFocusReason); + m_entryEditor->selectAll(); +} + +void ArchiveView::closeEntryEditor() +{ + m_entryEditor->removeEventFilter(this); + closePersistentEditor(m_editorIndex); + m_editorIndex = QModelIndex(); +} diff --git a/part/archiveview.h b/part/archiveview.h index 1f0a63a7..25ffa4f6 100644 --- a/part/archiveview.h +++ b/part/archiveview.h @@ -1,41 +1,57 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2008 Harald Hvaal * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #ifndef ARCHIVEVIEW_H #define ARCHIVEVIEW_H #include +#include class ArchiveView : public QTreeView { Q_OBJECT public: explicit ArchiveView(QWidget *parent = 0); virtual void dragEnterEvent(class QDragEnterEvent * event) Q_DECL_OVERRIDE; virtual void dropEvent(class QDropEvent * event) Q_DECL_OVERRIDE; virtual void dragMoveEvent(class QDragMoveEvent * event) Q_DECL_OVERRIDE; virtual void startDrag(Qt::DropActions supportedActions) Q_DECL_OVERRIDE; void setModel(QAbstractItemModel *model) Q_DECL_OVERRIDE; + + void openEntryEditor(QModelIndex index); + +protected: + virtual bool eventFilter(QObject *object, QEvent *event) Q_DECL_OVERRIDE; + virtual void mouseReleaseEvent(QMouseEvent *event) Q_DECL_OVERRIDE; + virtual void keyPressEvent(QKeyEvent *event) Q_DECL_OVERRIDE; + +signals: + void entryChanged(QString name); + +private: + void closeEntryEditor(); + QModelIndex m_editorIndex; + QLineEdit *m_entryEditor; }; #endif /* ARCHIVEVIEW_H */ diff --git a/part/overwritedialog.cpp b/part/overwritedialog.cpp new file mode 100644 index 00000000..a087671b --- /dev/null +++ b/part/overwritedialog.cpp @@ -0,0 +1,74 @@ +/* + * ark -- archiver for the KDE project + * + * Copyright (C) 2016 Ragnar Thomsen + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "overwritedialog.h" + +namespace Kerfuffle +{ + +OverwriteDialog::OverwriteDialog(QWidget *parent, const QList &entries, const QHash &icons, bool error) + : QDialog(parent) + , m_buttonBox(Qt::Horizontal) + , m_okButton(i18n("OK")) + , m_cancelButton(i18n("Cancel")) +{ + m_vBoxLayout.addLayout(&m_messageLayout); + m_vBoxLayout.addWidget(&m_entriesList); + m_vBoxLayout.addWidget(&m_buttonBox); + + m_messageLayout.addWidget(&m_messageIcon); + m_messageLayout.addWidget(&m_messageText); + + m_messageIcon.setPixmap(QIcon::fromTheme(QStringLiteral("dialog-warning")).pixmap(QSize(64, 64))); + if (error) { + m_messageText.setText(i18n("Files with the following paths already exist. Remove them if you really want to overwrite.")); + } + else { + m_okButton.setIcon(QIcon::fromTheme(QStringLiteral("dialog-ok"))); + m_messageText.setText(i18n("Files with the following paths already exist. Do you want to continue overwriting them?")); + m_buttonBox.addButton(&m_okButton, QDialogButtonBox::AcceptRole); + } + m_cancelButton.setIcon(QIcon::fromTheme(QStringLiteral("dialog-cancel"))); + m_buttonBox.addButton(&m_cancelButton, QDialogButtonBox::RejectRole); + + connect(&m_buttonBox, SIGNAL(accepted()), this, SLOT(accept())); + connect(&m_buttonBox, SIGNAL(rejected()), this, SLOT(reject())); + + foreach (const Archive::Entry *entry, entries) { + QListWidgetItem *item = new QListWidgetItem(icons.value(entry->fullPath(true)), entry->fullPath(true)); + m_entriesList.addItem(item); + } + + setLayout(&m_vBoxLayout); + setFixedSize(window()->sizeHint()); +} + +OverwriteDialog::~OverwriteDialog() +{ +} + +} diff --git a/part/overwritedialog.h b/part/overwritedialog.h new file mode 100644 index 00000000..3e9a927e --- /dev/null +++ b/part/overwritedialog.h @@ -0,0 +1,69 @@ +/* + * ark -- archiver for the KDE project + * + * Copyright (C) 2016 Ragnar Thomsen + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef OVERWRITEDIALOG_H +#define OVERWRITEDIALOG_H + +#include "kerfuffle_export.h" +#include "kerfuffle/archiveentry.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +class QUrl; + +namespace Kerfuffle +{ +class KERFUFFLE_EXPORT OverwriteDialog : public QDialog +{ + Q_OBJECT +public: + explicit OverwriteDialog(QWidget *parent, const QList &entries, const QHash &icons, bool error = false); + virtual ~OverwriteDialog(); + +private: + QVBoxLayout m_vBoxLayout; + QHBoxLayout m_messageLayout; + QLabel m_messageIcon; + QLabel m_messageText; + QListWidget m_entriesList; + QDialogButtonBox m_buttonBox; + QPushButton m_okButton; + QPushButton m_cancelButton; +}; +} + +#endif diff --git a/part/part.cpp b/part/part.cpp index 5c24b75c..6bee9791 100644 --- a/part/part.cpp +++ b/part/part.cpp @@ -1,1446 +1,1644 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 Harald Hvaal * Copyright (C) 2009-2012 Raphael Kubo da Costa * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "part.h" #include "ark_debug.h" #include "adddialog.h" +#include "overwritedialog.h" #include "archiveformat.h" #include "archivemodel.h" #include "archiveview.h" #include "arkviewer.h" #include "dnddbusinterfaceadaptor.h" #include "infopanel.h" #include "jobtracker.h" #include "kerfuffle/extractiondialog.h" #include "kerfuffle/extractionsettingspage.h" #include "kerfuffle/jobs.h" #include "kerfuffle/settings.h" #include "kerfuffle/previewsettingspage.h" #include "kerfuffle/propertiesdialog.h" #include "pluginmanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kerfuffle; K_PLUGIN_FACTORY_WITH_JSON(Factory, "ark_part.json", registerPlugin();) namespace Ark { static quint32 s_instanceCounter = 1; Part::Part(QWidget *parentWidget, QObject *parent, const QVariantList& args) : KParts::ReadWritePart(parent), m_splitter(Q_NULLPTR), m_busy(false), m_jobTracker(Q_NULLPTR) { Q_UNUSED(args) setComponentData(*createAboutData(), false); new DndExtractAdaptor(this); const QString pathName = QStringLiteral("/DndExtract/%1").arg(s_instanceCounter++); if (!QDBusConnection::sessionBus().registerObject(pathName, this)) { qCCritical(ARK) << "Could not register a D-Bus object for drag'n'drop"; } // m_vlayout is needed for later insertion of QMessageWidget QWidget *mainWidget = new QWidget; m_vlayout = new QVBoxLayout; m_model = new ArchiveModel(pathName, this); m_splitter = new QSplitter(Qt::Horizontal, parentWidget); m_view = new ArchiveView; m_infoPanel = new InfoPanel(m_model); // Add widgets for the comment field. m_commentView = new QPlainTextEdit(); m_commentView->setReadOnly(true); m_commentView->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); m_commentBox = new QGroupBox(i18n("Comment")); m_commentBox->hide(); QVBoxLayout *vbox = new QVBoxLayout; vbox->addWidget(m_commentView); m_commentBox->setLayout(vbox); m_messageWidget = new KMessageWidget(parentWidget); m_messageWidget->hide(); m_commentMsgWidget = new KMessageWidget(); m_commentMsgWidget->setText(i18n("Comment has been modified.")); m_commentMsgWidget->setMessageType(KMessageWidget::Information); m_commentMsgWidget->setCloseButtonVisible(false); m_commentMsgWidget->hide(); QAction *saveAction = new QAction(i18n("Save"), m_commentMsgWidget); m_commentMsgWidget->addAction(saveAction); connect(saveAction, &QAction::triggered, this, &Part::slotAddComment); m_commentBox->layout()->addWidget(m_commentMsgWidget); connect(m_commentView, &QPlainTextEdit::textChanged, this, &Part::slotCommentChanged); setWidget(mainWidget); mainWidget->setLayout(m_vlayout); // Configure the QVBoxLayout and add widgets m_vlayout->setContentsMargins(0,0,0,0); m_vlayout->addWidget(m_messageWidget); m_vlayout->addWidget(m_splitter); // Vertical QSplitter for the file view and comment field. m_commentSplitter = new QSplitter(Qt::Vertical, parentWidget); m_commentSplitter->setOpaqueResize(false); m_commentSplitter->addWidget(m_view); m_commentSplitter->addWidget(m_commentBox); m_commentSplitter->setCollapsible(0, false); // Horizontal QSplitter for the file view and infopanel. m_splitter->addWidget(m_commentSplitter); m_splitter->addWidget(m_infoPanel); // Read settings from config file and show/hide infoPanel. if (!ArkSettings::showInfoPanel()) { m_infoPanel->hide(); } else { m_splitter->setSizes(ArkSettings::splitterSizes()); } setupView(); setupActions(); + connect(m_view, &ArchiveView::entryChanged, + this, &Part::slotRenameFile); + connect(m_model, &ArchiveModel::loadingStarted, this, &Part::slotLoadingStarted); connect(m_model, &ArchiveModel::loadingFinished, this, &Part::slotLoadingFinished); connect(m_model, &ArchiveModel::droppedFiles, - this, static_cast(&Part::slotAddFiles)); + this, static_cast(&Part::slotAddFiles)); connect(m_model, &ArchiveModel::error, this, &Part::slotError); connect(this, &Part::busy, this, &Part::setBusyGui); connect(this, &Part::ready, this, &Part::setReadyGui); connect(this, static_cast(&KParts::ReadOnlyPart::completed), this, &Part::setFileNameFromArchive); m_statusBarExtension = new KParts::StatusBarExtension(this); setXMLFile(QStringLiteral("ark_part.rc")); } Part::~Part() { qDeleteAll(m_tmpOpenDirList); // Only save splitterSizes if infopanel is visible, // because we don't want to store zero size for infopanel. if (m_showInfoPanelAction->isChecked()) { ArkSettings::setSplitterSizes(m_splitter->sizes()); } ArkSettings::setShowInfoPanel(m_showInfoPanelAction->isChecked()); ArkSettings::self()->save(); m_extractArchiveAction->menu()->deleteLater(); m_extractAction->menu()->deleteLater(); } void Part::slotCommentChanged() { if (!m_model->archive()) { return; } if (m_commentMsgWidget->isHidden() && m_commentView->toPlainText() != m_model->archive()->comment()) { m_commentMsgWidget->animatedShow(); } else if (m_commentMsgWidget->isVisible() && m_commentView->toPlainText() == m_model->archive()->comment()) { m_commentMsgWidget->hide(); } } KAboutData *Part::createAboutData() { return new KAboutData(QStringLiteral("ark"), i18n("ArkPart"), QStringLiteral("3.0")); } void Part::registerJob(KJob* job) { if (!m_jobTracker) { m_jobTracker = new JobTracker(widget()); m_statusBarExtension->addStatusBarItem(m_jobTracker->widget(0), 0, true); m_jobTracker->widget(job)->show(); } m_jobTracker->registerJob(job); emit busy(); connect(job, &KJob::result, this, &Part::ready); } // TODO: KIO::mostLocalHere is used here to resolve some KIO URLs to local // paths (e.g. desktop:/), but more work is needed to support extraction // to non-local destinations. See bugs #189322 and #204323. void Part::extractSelectedFilesTo(const QString& localPath) { if (!m_model) { return; } const QUrl url = QUrl::fromUserInput(localPath, QString()); KIO::StatJob* statJob = nullptr; // Try to resolve the URL to a local path. if (!url.isLocalFile() && !url.scheme().isEmpty()) { statJob = KIO::mostLocalUrl(url); if (!statJob->exec() || statJob->error() != 0) { return; } } const QString destination = statJob ? statJob->statResult().stringValue(KIO::UDSEntry::UDS_LOCAL_PATH) : localPath; delete statJob; // The URL could not be resolved to a local path. if (!url.isLocalFile() && destination.isEmpty()) { qCWarning(ARK) << "Ark cannot extract to non-local destination:" << localPath; KMessageBox::sorry(widget(), xi18nc("@info", "Ark can only extract to local destinations.")); return; } qCDebug(ARK) << "Extract to" << destination; Kerfuffle::ExtractionOptions options; options[QStringLiteral("PreservePaths")] = true; options[QStringLiteral("RemoveRootNode")] = true; options[QStringLiteral("DragAndDrop")] = true; // Create and start the ExtractJob. ExtractJob *job = m_model->extractFiles(filesAndRootNodesForIndexes(addChildren(m_view->selectionModel()->selectedRows())), destination, options); registerJob(job); connect(job, &KJob::result, this, &Part::slotExtractionDone); job->start(); } void Part::setupView() { m_view->setContextMenuPolicy(Qt::CustomContextMenu); m_view->setModel(m_model); m_view->setSortingEnabled(true); connect(m_view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Part::updateActions); connect(m_view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Part::selectionChanged); connect(m_view, &QTreeView::activated, this, &Part::slotActivated); connect(m_view, &QWidget::customContextMenuRequested, this, &Part::slotShowContextMenu); connect(m_model, &QAbstractItemModel::columnsInserted, this, &Part::adjustColumns); } void Part::slotActivated(QModelIndex) { // The activated signal is emitted when items are selected with the mouse, // so do nothing if CTRL or SHIFT key is pressed. if (QGuiApplication::keyboardModifiers() != Qt::ShiftModifier && QGuiApplication::keyboardModifiers() != Qt::ControlModifier) { ArkSettings::defaultOpenAction() == ArkSettings::EnumDefaultOpenAction::Preview ? slotOpenEntry(Preview) : slotOpenEntry(OpenFile); } } void Part::setupActions() { // We use a QSignalMapper for the preview, open and openwith actions. This // way we can connect all three actions to the same slot slotOpenEntry and // pass the OpenFileMode as argument to the slot. m_signalMapper = new QSignalMapper; m_showInfoPanelAction = new KToggleAction(i18nc("@action:inmenu", "Show information panel"), this); actionCollection()->addAction(QStringLiteral( "show-infopanel" ), m_showInfoPanelAction); m_showInfoPanelAction->setChecked(ArkSettings::showInfoPanel()); connect(m_showInfoPanelAction, &QAction::triggered, this, &Part::slotToggleInfoPanel); m_saveAsAction = actionCollection()->addAction(KStandardAction::SaveAs, QStringLiteral("ark_file_save_as"), this, SLOT(slotSaveAs())); m_openFileAction = actionCollection()->addAction(QStringLiteral("openfile")); m_openFileAction->setText(i18nc("open a file with external program", "&Open")); m_openFileAction->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); m_openFileAction->setToolTip(i18nc("@info:tooltip", "Click to open the selected file with the associated application")); - connect(m_openFileAction, SIGNAL(triggered(bool)), m_signalMapper, SLOT(map())); + connect(m_openFileAction, &QAction::triggered, m_signalMapper, static_cast(&QSignalMapper::map)); m_signalMapper->setMapping(m_openFileAction, OpenFile); m_openFileWithAction = actionCollection()->addAction(QStringLiteral("openfilewith")); m_openFileWithAction->setText(i18nc("open a file with external program", "Open &With...")); m_openFileWithAction->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); m_openFileWithAction->setToolTip(i18nc("@info:tooltip", "Click to open the selected file with an external program")); - connect(m_openFileWithAction, SIGNAL(triggered(bool)), m_signalMapper, SLOT(map())); + connect(m_openFileWithAction, &QAction::triggered, m_signalMapper, static_cast(&QSignalMapper::map)); m_signalMapper->setMapping(m_openFileWithAction, OpenFileWith); m_previewAction = actionCollection()->addAction(QStringLiteral("preview")); m_previewAction->setText(i18nc("to preview a file inside an archive", "Pre&view")); m_previewAction->setIcon(QIcon::fromTheme(QStringLiteral("document-preview-archive"))); m_previewAction->setToolTip(i18nc("@info:tooltip", "Click to preview the selected file")); actionCollection()->setDefaultShortcut(m_previewAction, Qt::CTRL + Qt::Key_P); - connect(m_previewAction, SIGNAL(triggered(bool)), m_signalMapper, SLOT(map())); + connect(m_previewAction, &QAction::triggered, m_signalMapper, static_cast(&QSignalMapper::map)); m_signalMapper->setMapping(m_previewAction, Preview); m_extractArchiveAction = actionCollection()->addAction(QStringLiteral("extract_all")); m_extractArchiveAction->setText(i18nc("@action:inmenu", "E&xtract All")); m_extractArchiveAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract"))); m_extractArchiveAction->setToolTip(i18n("Click to open an extraction dialog, where you can choose how to extract all the files in the archive")); actionCollection()->setDefaultShortcut(m_extractArchiveAction, Qt::CTRL + Qt::SHIFT + Qt::Key_E); - connect(m_extractArchiveAction, &QAction::triggered, - this, &Part::slotExtractArchive); + connect(m_extractArchiveAction, &QAction::triggered, this, &Part::slotExtractArchive); m_extractAction = actionCollection()->addAction(QStringLiteral("extract")); m_extractAction->setText(i18nc("@action:inmenu", "&Extract")); m_extractAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract"))); actionCollection()->setDefaultShortcut(m_extractAction, Qt::CTRL + Qt::Key_E); m_extractAction->setToolTip(i18n("Click to open an extraction dialog, where you can choose to extract either all files or just the selected ones")); - connect(m_extractAction, &QAction::triggered, - this, &Part::slotShowExtractionDialog); + connect(m_extractAction, &QAction::triggered, this, &Part::slotShowExtractionDialog); m_addFilesAction = actionCollection()->addAction(QStringLiteral("add")); m_addFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-insert"))); m_addFilesAction->setText(i18n("Add &Files to root...")); m_addFilesAction->setToolTip(i18nc("@info:tooltip", "Click to add files to the archive")); - connect(m_addFilesAction, SIGNAL(triggered(bool)), - this, SLOT(slotAddFiles())); + connect(m_addFilesAction, &QAction::triggered, this, static_cast(&Part::slotAddFiles)); m_addFilesToAction = actionCollection()->addAction(QStringLiteral("addto")); m_addFilesToAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-insert"))); m_addFilesToAction->setText(i18n("Add &Files to...")); m_addFilesToAction->setToolTip(i18nc("@info:tooltip", "Click to add files to the archive")); -// connect(m_addFilesAction, SIGNAL(triggered(bool)), -// this, SLOT(slotAddFiles())); + connect(m_addFilesToAction, &QAction::triggered, this, static_cast(&Part::slotAddFiles)); m_renameFileAction = actionCollection()->addAction(QStringLiteral("rename")); m_renameFileAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename"))); m_renameFileAction->setText(i18n("&Rename")); actionCollection()->setDefaultShortcut(m_renameFileAction, Qt::Key_F2); m_renameFileAction->setToolTip(i18nc("@info:tooltip", "Click to rename the selected file")); -// connect(m_renameFileAction, &QAction::triggered, -// this, &Part::slotShowProperties); + connect(m_renameFileAction, &QAction::triggered, this, &Part::slotEditFileName); m_deleteFilesAction = actionCollection()->addAction(QStringLiteral("delete")); m_deleteFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-remove"))); m_deleteFilesAction->setText(i18n("De&lete")); actionCollection()->setDefaultShortcut(m_deleteFilesAction, Qt::Key_Delete); m_deleteFilesAction->setToolTip(i18nc("@info:tooltip", "Click to delete the selected files")); - connect(m_deleteFilesAction, &QAction::triggered, - this, &Part::slotDeleteFiles); + connect(m_deleteFilesAction, &QAction::triggered, this, &Part::slotDeleteFiles); m_cutFilesAction = actionCollection()->addAction(QStringLiteral("cut")); m_cutFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-cut"))); m_cutFilesAction->setText(i18nc("@action:inmenu", "C&ut")); actionCollection()->setDefaultShortcut(m_cutFilesAction, Qt::CTRL + Qt::Key_X); m_cutFilesAction->setToolTip(i18nc("@info:tooltip", "Click to cut the selected files")); -// connect(m_cutFilesAction, &QAction::triggered, -// this, &Part::slotShowProperties); + connect(m_cutFilesAction, &QAction::triggered, this, &Part::slotCutFiles); m_copyFilesAction = actionCollection()->addAction(QStringLiteral("copy")); m_copyFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy"))); m_copyFilesAction->setText(i18nc("@action:inmenu", "C&opy")); actionCollection()->setDefaultShortcut(m_copyFilesAction, Qt::CTRL + Qt::Key_C); m_copyFilesAction->setToolTip(i18nc("@info:tooltip", "Click to copy the selected files")); -// connect(m_copyFilesAction, &QAction::triggered, -// this, &Part::slotShowProperties); + connect(m_copyFilesAction, &QAction::triggered, this, &Part::slotCopyFiles); m_pasteFilesAction = actionCollection()->addAction(QStringLiteral("paste")); m_pasteFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-paste"))); m_pasteFilesAction->setText(i18nc("@action:inmenu", "Pa&ste")); - actionCollection()->setDefaultShortcut(m_pasteFilesAction, Qt::CTRL + Qt::Key_P); + actionCollection()->setDefaultShortcut(m_pasteFilesAction, Qt::CTRL + Qt::Key_V); m_pasteFilesAction->setToolTip(i18nc("@info:tooltip", "Click to paste the files here")); -// connect(m_pasteFilesAction, &QAction::triggered, -// this, &Part::slotShowProperties); + connect(m_pasteFilesAction, &QAction::triggered, this, static_cast(&Part::slotPasteFiles)); m_propertiesAction = actionCollection()->addAction(QStringLiteral("properties")); m_propertiesAction->setIcon(QIcon::fromTheme(QStringLiteral("document-properties"))); m_propertiesAction->setText(i18nc("@action:inmenu", "&Properties")); actionCollection()->setDefaultShortcut(m_propertiesAction, Qt::ALT + Qt::Key_Return); m_propertiesAction->setToolTip(i18nc("@info:tooltip", "Click to see properties for archive")); - connect(m_propertiesAction, &QAction::triggered, - this, &Part::slotShowProperties); + connect(m_propertiesAction, &QAction::triggered, this, &Part::slotShowProperties); m_editCommentAction = actionCollection()->addAction(QStringLiteral("edit_comment")); m_editCommentAction->setIcon(QIcon::fromTheme(QStringLiteral("document-edit"))); actionCollection()->setDefaultShortcut(m_editCommentAction, Qt::ALT + Qt::Key_C); m_editCommentAction->setToolTip(i18nc("@info:tooltip", "Click to add or edit comment")); connect(m_editCommentAction, &QAction::triggered, this, &Part::slotShowComment); m_testArchiveAction = actionCollection()->addAction(QStringLiteral("test_archive")); m_testArchiveAction->setIcon(QIcon::fromTheme(QStringLiteral("checkmark"))); m_testArchiveAction->setText(i18nc("@action:inmenu", "&Test Integrity")); actionCollection()->setDefaultShortcut(m_testArchiveAction, Qt::ALT + Qt::Key_T); m_testArchiveAction->setToolTip(i18nc("@info:tooltip", "Click to test the archive for integrity")); connect(m_testArchiveAction, &QAction::triggered, this, &Part::slotTestArchive); connect(m_signalMapper, static_cast(&QSignalMapper::mapped), this, &Part::slotOpenEntry); updateActions(); updateQuickExtractMenu(m_extractArchiveAction); updateQuickExtractMenu(m_extractAction); } void Part::updateActions() { bool isWritable = m_model->archive() && !m_model->archive()->isReadOnly(); const Archive::Entry *entry = m_model->entryForIndex(m_view->selectionModel()->currentIndex()); int selectedEntriesCount = m_view->selectionModel()->selectedRows().count(); // Figure out if entry size is larger than preview size limit. const int maxPreviewSize = ArkSettings::previewFileSizeLimit() * 1024 * 1024; const bool limit = ArkSettings::limitPreviewFileSize(); bool isPreviewable = (!limit || (limit && entry != Q_NULLPTR && entry->property("size").toLongLong() < maxPreviewSize)); + const bool isDir = (entry == Q_NULLPTR) ? false : entry->isDir(); m_previewAction->setEnabled(!isBusy() && isPreviewable && - !entry->isDir() && + !isDir && (selectedEntriesCount == 1)); m_extractArchiveAction->setEnabled(!isBusy() && (m_model->rowCount() > 0)); m_extractAction->setEnabled(!isBusy() && (m_model->rowCount() > 0)); m_saveAsAction->setEnabled(!isBusy() && m_model->rowCount() > 0); m_addFilesAction->setEnabled(!isBusy() && isWritable); m_addFilesToAction->setEnabled(!isBusy() && isWritable && - entry->isDir() && + isDir && (selectedEntriesCount == 1)); m_deleteFilesAction->setEnabled(!isBusy() && isWritable && (selectedEntriesCount > 0)); m_openFileAction->setEnabled(!isBusy() && isPreviewable && - !entry->isDir() && + !isDir && (selectedEntriesCount == 1)); m_openFileWithAction->setEnabled(!isBusy() && isPreviewable && - !entry->isDir() && + !isDir && (selectedEntriesCount == 1)); m_propertiesAction->setEnabled(!isBusy() && m_model->archive()); m_renameFileAction->setEnabled(!isBusy() && isWritable && (selectedEntriesCount == 1)); m_cutFilesAction->setEnabled(!isBusy() && isWritable && (selectedEntriesCount > 0)); m_copyFilesAction->setEnabled(!isBusy() && isWritable && (selectedEntriesCount > 0)); m_pasteFilesAction->setEnabled(!isBusy() && isWritable && - entry->isDir() && - (selectedEntriesCount == 1) && - (m_filesToMoveOrCopy.count() > 0)); + (selectedEntriesCount == 0 || (selectedEntriesCount == 1 && isDir)) && + (m_filesToMove.count() > 0 || m_filesToCopy.count() > 0)); m_commentView->setEnabled(!isBusy()); m_commentMsgWidget->setEnabled(!isBusy()); m_editCommentAction->setEnabled(false); m_testArchiveAction->setEnabled(false); if (m_model->archive()) { const KPluginMetaData metadata = PluginManager().preferredPluginFor(m_model->archive()->mimeType())->metaData(); bool supportsWriteComment = ArchiveFormat::fromMetadata(m_model->archive()->mimeType(), metadata).supportsWriteComment(); m_editCommentAction->setEnabled(!isBusy() && supportsWriteComment); m_commentView->setReadOnly(!supportsWriteComment); m_editCommentAction->setText(m_model->archive()->hasComment() ? i18nc("@action:inmenu mutually exclusive with Add &Comment", "Edit &Comment") : i18nc("@action:inmenu mutually exclusive with Edit &Comment", "Add &Comment")); bool supportsTesting = ArchiveFormat::fromMetadata(m_model->archive()->mimeType(), metadata).supportsTesting(); m_testArchiveAction->setEnabled(!isBusy() && supportsTesting); } else { m_commentView->setReadOnly(true); m_editCommentAction->setText(i18nc("@action:inmenu mutually exclusive with Edit &Comment", "Add &Comment")); } } void Part::slotShowComment() { if (!m_commentBox->isVisible()) { m_commentBox->show(); m_commentSplitter->setSizes(QList() << m_view->height() * 0.6 << 1); } m_commentView->setFocus(); } void Part::slotAddComment() { CommentJob *job = m_model->archive()->addComment(m_commentView->toPlainText()); if (!job) { return; } registerJob(job); job->start(); m_commentMsgWidget->hide(); if (m_commentView->toPlainText().isEmpty()) { m_commentBox->hide(); } } void Part::slotTestArchive() { TestJob *job = m_model->archive()->testArchive(); if (!job) { return; } registerJob(job); connect(job, &KJob::result, this, &Part::slotTestingDone); job->start(); } void Part::resetGui() { m_messageWidget->hide(); m_commentView->clear(); m_commentBox->hide(); } void Part::slotTestingDone(KJob* job) { if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } else if (static_cast(job)->testSucceeded()) { KMessageBox::information(widget(), i18n("The archive passed the integrity test."), i18n("Test Results")); } else { KMessageBox::error(widget(), i18n("The archive failed the integrity test."), i18n("Test Results")); } } void Part::updateQuickExtractMenu(QAction *extractAction) { if (!extractAction) { return; } QMenu *menu = extractAction->menu(); if (!menu) { menu = new QMenu(); extractAction->setMenu(menu); connect(menu, &QMenu::triggered, this, &Part::slotQuickExtractFiles); // Remember to keep this action's properties as similar to // extractAction's as possible (except where it does not make // sense, such as the text or the shortcut). QAction *extractTo = menu->addAction(i18n("Extract To...")); extractTo->setIcon(extractAction->icon()); extractTo->setToolTip(extractAction->toolTip()); if (extractAction == m_extractArchiveAction) { connect(extractTo, &QAction::triggered, this, &Part::slotExtractArchive); } else { connect(extractTo, &QAction::triggered, this, &Part::slotShowExtractionDialog); } menu->addSeparator(); QAction *header = menu->addAction(i18n("Quick Extract To...")); header->setEnabled(false); header->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract"))); } while (menu->actions().size() > 3) { menu->removeAction(menu->actions().last()); } const KConfigGroup conf(KSharedConfig::openConfig(), "ExtractDialog"); const QStringList dirHistory = conf.readPathEntry("DirHistory", QStringList()); for (int i = 0; i < qMin(10, dirHistory.size()); ++i) { const QString dir = QUrl(dirHistory.value(i)).toString(QUrl::RemoveScheme | QUrl::NormalizePathSegments | QUrl::PreferLocalFile); if (QDir(dir).exists()) { QAction *newAction = menu->addAction(dir); newAction->setData(dir); } } } void Part::slotQuickExtractFiles(QAction *triggeredAction) { // #190507: triggeredAction->data.isNull() means it's the "Extract to..." // action, and we do not want it to run here if (!triggeredAction->data().isNull()) { const QString userDestination = triggeredAction->data().toString(); qCDebug(ARK) << "Extract to user dest" << userDestination; QString finalDestinationDirectory; const QString detectedSubfolder = detectSubfolder(); qCDebug(ARK) << "Detected subfolder" << detectedSubfolder; if (!isSingleFolderArchive()) { finalDestinationDirectory = userDestination + QDir::separator() + detectedSubfolder; QDir(userDestination).mkdir(detectedSubfolder); } else { finalDestinationDirectory = userDestination; } qCDebug(ARK) << "Extract to final dest" << finalDestinationDirectory; Kerfuffle::ExtractionOptions options; options[QStringLiteral("PreservePaths")] = true; QList files = filesAndRootNodesForIndexes(m_view->selectionModel()->selectedRows()); ExtractJob *job = m_model->extractFiles(files, finalDestinationDirectory, options); registerJob(job); connect(job, &KJob::result, this, &Part::slotExtractionDone); job->start(); } } void Part::selectionChanged() { m_infoPanel->setIndexes(m_view->selectionModel()->selectedRows()); } bool Part::openFile() { qCDebug(ARK) << "Attempting to open archive" << localFilePath(); resetGui(); if (!isLocalFileValid()) { return false; } const QString fixedMimeType = arguments().metaData()[QStringLiteral("fixedMimeType")]; QScopedPointer archive(Kerfuffle::Archive::create(localFilePath(), fixedMimeType, m_model)); Q_ASSERT(archive); if (archive->error() == NoPlugin) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Ark was not able to open %1. No suitable plugin found." "Ark does not seem to support this file type.", QFileInfo(localFilePath()).fileName())); return false; } if (archive->error() == FailedPlugin) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Ark was not able to open %1. Failed to load a suitable plugin." "Make sure any executables needed to handle the archive type are installed.", QFileInfo(localFilePath()).fileName())); return false; } Q_ASSERT(archive->isValid()); // Plugin loaded successfully. KJob *job = m_model->setArchive(archive.take()); if (job) { registerJob(job); job->start(); } else { updateActions(); } m_infoPanel->setIndex(QModelIndex()); if (arguments().metaData()[QStringLiteral("showExtractDialog")] == QLatin1String("true")) { QTimer::singleShot(0, this, &Part::slotShowExtractionDialog); } const QString password = arguments().metaData()[QStringLiteral("encryptionPassword")]; if (!password.isEmpty()) { m_model->encryptArchive(password, arguments().metaData()[QStringLiteral("encryptHeader")] == QLatin1String("true")); } return true; } bool Part::saveFile() { return true; } bool Part::isBusy() const { return m_busy; } KConfigSkeleton *Part::config() const { return ArkSettings::self(); } QList Part::settingsPages(QWidget *parent) const { QList pages; pages.append(new ExtractionSettingsPage(parent, i18nc("@title:tab", "Extraction Settings"), QStringLiteral("archive-extract"))); pages.append(new PreviewSettingsPage(parent, i18nc("@title:tab", "Preview Settings"), QStringLiteral("document-preview-archive"))); return pages; } bool Part::isLocalFileValid() { const QString localFile = localFilePath(); const QFileInfo localFileInfo(localFile); const bool creatingNewArchive = arguments().metaData()[QStringLiteral("createNewArchive")] == QLatin1String("true"); if (localFileInfo.isDir()) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "%1 is a directory.", localFile)); return false; } if (creatingNewArchive) { if (localFileInfo.exists()) { if (!confirmAndDelete(localFile)) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Could not overwrite %1. Check whether you have write permission.", localFile)); return false; } } displayMsgWidget(KMessageWidget::Information, xi18nc("@info", "The archive %1 will be created as soon as you add a file.", localFile)); } else { if (!localFileInfo.exists()) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "The archive %1 was not found.", localFile)); return false; } if (!localFileInfo.isReadable()) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "The archive %1 could not be loaded, as it was not possible to read from it.", localFile)); return false; } } return true; } bool Part::confirmAndDelete(const QString &targetFile) { QFileInfo targetInfo(targetFile); const auto buttonCode = KMessageBox::warningYesNo(widget(), xi18nc("@info", "The archive %1 already exists. Do you wish to overwrite it?", targetInfo.fileName()), i18nc("@title:window", "File Exists"), KGuiItem(i18nc("@action:button", "Overwrite")), KStandardGuiItem::cancel()); if (buttonCode != KMessageBox::Yes || !targetInfo.isWritable()) { return false; } qCDebug(ARK) << "Removing file" << targetFile; return QFile(targetFile).remove(); } void Part::slotLoadingStarted() { + m_filesToMove.clear(); + m_filesToCopy.clear(); } void Part::slotLoadingFinished(KJob *job) { if (job->error()) { if (arguments().metaData()[QStringLiteral("createNewArchive")] != QLatin1String("true")) { if (job->error() != KJob::KilledJobError) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Loading the archive %1 failed with the following error:%2", localFilePath(), job->errorText())); } // The file failed to open, so reset the open archive, info panel and caption. m_model->setArchive(Q_NULLPTR); m_infoPanel->setPrettyFileName(QString()); m_infoPanel->updateWithDefaults(); emit setWindowCaption(QString()); } } m_view->sortByColumn(0, Qt::AscendingOrder); // #303708: expand the first level only when there is just one root folder. // Typical use case: an archive with source files. if (m_view->model()->rowCount() == 1) { m_view->expandToDepth(0); } // After loading all files, resize the columns to fit all fields m_view->header()->resizeSections(QHeaderView::ResizeToContents); updateActions(); if (!m_model->archive()) { return; } if (!m_model->archive()->comment().isEmpty()) { m_commentView->setPlainText(m_model->archive()->comment()); slotShowComment(); } else { m_commentView->clear(); m_commentBox->hide(); } if (m_model->rowCount() == 0) { qCWarning(ARK) << "No entry listed by the plugin"; displayMsgWidget(KMessageWidget::Warning, xi18nc("@info", "The archive is empty or Ark could not open its content.")); } else if (m_model->rowCount() == 1) { if (m_model->archive()->mimeType().inherits(QStringLiteral("application/x-cd-image")) && m_model->entryForIndex(m_model->index(0, 0))->fullPath() == QLatin1String("README.TXT")) { qCWarning(ARK) << "Detected ISO image with UDF filesystem"; displayMsgWidget(KMessageWidget::Warning, xi18nc("@info", "Ark does not currently support ISO files with UDF filesystem.")); } } } void Part::setReadyGui() { QApplication::restoreOverrideCursor(); m_busy = false; if (m_statusBarExtension->statusBar()) { m_statusBarExtension->statusBar()->hide(); } m_view->setEnabled(true); updateActions(); } void Part::setBusyGui() { QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); m_busy = true; if (m_statusBarExtension->statusBar()) { m_statusBarExtension->statusBar()->show(); } m_view->setEnabled(false); updateActions(); } void Part::setFileNameFromArchive() { const QString prettyName = url().fileName(); m_infoPanel->setPrettyFileName(prettyName); m_infoPanel->updateWithDefaults(); emit setWindowCaption(prettyName); } void Part::slotOpenEntry(int mode) { qCDebug(ARK) << "Opening with mode" << mode; QModelIndex index = m_view->selectionModel()->currentIndex(); Archive::Entry *entry = m_model->entryForIndex(index); // Don't open directories. if (entry->isDir()) { return; } // We don't support opening symlinks. if (!entry->property("link").toString().isEmpty()) { displayMsgWidget(KMessageWidget::Information, i18n("Ark cannot open symlinks.")); return; } // Extract the entry. if (!entry->fullPath().isEmpty()) { m_openFileMode = static_cast(mode); KJob *job = Q_NULLPTR; if (m_openFileMode == Preview) { job = m_model->preview(entry); connect(job, &KJob::result, this, &Part::slotPreviewExtractedEntry); } else { job = (m_openFileMode == OpenFile) ? m_model->open(entry) : m_model->openWith(entry); connect(job, &KJob::result, this, &Part::slotOpenExtractedEntry); } registerJob(job); job->start(); } } void Part::slotOpenExtractedEntry(KJob *job) { if (!job->error()) { OpenJob *openJob = qobject_cast(job); Q_ASSERT(openJob); // Since the user could modify the file (unlike the Preview case), // we'll need to manually delete the temp dir in the Part destructor. m_tmpOpenDirList << openJob->tempDir(); const QString fullName = openJob->validatedFilePath(); bool isWritable = m_model->archive() && !m_model->archive()->isReadOnly(); // If archive is readonly set temporarily extracted file to readonly as // well so user will be notified if trying to modify and save the file. if (!isWritable) { QFile::setPermissions(fullName, QFileDevice::ReadOwner | QFileDevice::ReadGroup | QFileDevice::ReadOther); } if (isWritable) { m_fileWatcher = new QFileSystemWatcher; connect(m_fileWatcher, &QFileSystemWatcher::fileChanged, this, &Part::slotWatchedFileModified); m_fileWatcher->addPath(fullName); } if (qobject_cast(job)) { const QList urls = {QUrl::fromUserInput(fullName, QString(), QUrl::AssumeLocalFile)}; KRun::displayOpenWithDialog(urls, widget()); } else { KRun::runUrl(QUrl::fromUserInput(fullName, QString(), QUrl::AssumeLocalFile), QMimeDatabase().mimeTypeForFile(fullName).name(), widget()); } } else if (job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } setReadyGui(); } void Part::slotPreviewExtractedEntry(KJob *job) { if (!job->error()) { PreviewJob *previewJob = qobject_cast(job); Q_ASSERT(previewJob); ArkViewer::view(previewJob->validatedFilePath()); } else if (job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } setReadyGui(); } void Part::slotWatchedFileModified(const QString& file) { qCDebug(ARK) << "Watched file modified:" << file; // Find the relative path of the file within the archive. QString relPath = file; foreach (QTemporaryDir *tmpDir, m_tmpOpenDirList) { relPath.remove(tmpDir->path()); //Remove tmpDir. } relPath = relPath.mid(1); //Remove leading slash. if (relPath.contains(QLatin1Char('/'))) { relPath = relPath.section(QLatin1Char('/'), 0, -2); //Remove filename. } else { // File is in the root of the archive, no path. relPath = QString(); } // Set up a string for display in KMessageBox. QString prettyFilename; if (relPath.isEmpty()) { prettyFilename = file.section(QLatin1Char('/'), -1); } else { prettyFilename = relPath + QLatin1Char('/') + file.section(QLatin1Char('/'), -1); } if (KMessageBox::questionYesNo(widget(), xi18n("The file %1 was modified. Do you want to update the archive?", prettyFilename), i18nc("@title:window", "File Modified")) == KMessageBox::Yes) { QStringList list = QStringList() << file; qCDebug(ARK) << "Updating file" << file << "with path" << relPath; - slotAddFiles(list, relPath); + slotAddFiles(list, Q_NULLPTR, relPath); } // This is needed because some apps, such as Kate, delete and recreate // files when saving. m_fileWatcher->addPath(file); } void Part::slotError(const QString& errorMessage, const QString& details) { if (details.isEmpty()) { KMessageBox::error(widget(), errorMessage); } else { KMessageBox::detailedError(widget(), errorMessage, details); } } bool Part::isSingleFolderArchive() const { return m_model->archive()->isSingleFolderArchive(); } QString Part::detectSubfolder() const { if (!m_model) { return QString(); } return m_model->archive()->subfolderName(); } void Part::slotExtractArchive() { if (m_view->selectionModel()->selectedRows().count() > 0) { m_view->selectionModel()->clear(); } slotShowExtractionDialog(); } void Part::slotShowExtractionDialog() { if (!m_model) { return; } QPointer dialog(new Kerfuffle::ExtractionDialog); dialog.data()->setModal(true); if (m_view->selectionModel()->selectedRows().count() > 0) { dialog.data()->setShowSelectedFiles(true); } dialog.data()->setSingleFolderArchive(isSingleFolderArchive()); dialog.data()->setSubfolder(detectSubfolder()); dialog.data()->setCurrentUrl(QUrl::fromLocalFile(QFileInfo(m_model->archive()->fileName()).absolutePath())); dialog.data()->show(); dialog.data()->restoreWindowSize(); if (dialog.data()->exec()) { updateQuickExtractMenu(m_extractArchiveAction); updateQuickExtractMenu(m_extractAction); QList files; // If the user has chosen to extract only selected entries, fetch these // from the QTreeView. if (!dialog.data()->extractAllFiles()) { files = filesAndRootNodesForIndexes(addChildren(m_view->selectionModel()->selectedRows())); } qCDebug(ARK) << "Selected " << files; Kerfuffle::ExtractionOptions options; if (dialog.data()->preservePaths()) { options[QStringLiteral("PreservePaths")] = true; } options[QStringLiteral("FollowExtractionDialogSettings")] = true; const QString destinationDirectory = dialog.data()->destinationDirectory().toDisplayString(QUrl::PreferLocalFile); ExtractJob *job = m_model->extractFiles(files, destinationDirectory, options); registerJob(job); connect(job, &KJob::result, this, &Part::slotExtractionDone); job->start(); } delete dialog.data(); } QModelIndexList Part::addChildren(const QModelIndexList &list) const { Q_ASSERT(m_model); QModelIndexList ret = list; // Iterate over indexes in list and add all children. for (int i = 0; i < ret.size(); ++i) { QModelIndex index = ret.at(i); for (int j = 0; j < m_model->rowCount(index); ++j) { QModelIndex child = m_model->index(j, 0, index); if (!ret.contains(child)) { ret << child; } } } return ret; } QList Part::filesForIndexes(const QModelIndexList& list) const { QList ret; foreach(const QModelIndex& index, list) { ret << m_model->entryForIndex(index); } return ret; } QList Part::filesAndRootNodesForIndexes(const QModelIndexList& list) const { QList fileList; QStringList fullPathsList; foreach (const QModelIndex& index, list) { // Find the topmost unselected parent. This is done by iterating up // through the directory hierarchy and see if each parent is included // in the selection OR if the parent is already part of list. // The latter is needed for unselected folders which are subfolders of // a selected parent folder. QModelIndex selectionRoot = index.parent(); while (m_view->selectionModel()->isSelected(selectionRoot) || list.contains(selectionRoot)) { selectionRoot = selectionRoot.parent(); } // Fetch the root node for the unselected parent. const QString rootFileName = selectionRoot.isValid() ? m_model->entryForIndex(selectionRoot)->fullPath() : QString(); // Append index with root node to fileList. QModelIndexList alist = QModelIndexList() << index; foreach (Archive::Entry *entry, filesForIndexes(alist)) { const QString fullPath = entry->fullPath(); if (!fullPathsList.contains(fullPath)) { entry->rootNode = rootFileName; fileList.append(entry); fullPathsList.append(fullPath); } } } return fileList; } void Part::slotExtractionDone(KJob* job) { if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } else { ExtractJob *extractJob = qobject_cast(job); Q_ASSERT(extractJob); const bool followExtractionDialogSettings = extractJob->extractionOptions().value(QStringLiteral("FollowExtractionDialogSettings"), false).toBool(); if (!followExtractionDialogSettings) { return; } if (ArkSettings::openDestinationFolderAfterExtraction()) { qCDebug(ARK) << "Shall open" << extractJob->destinationDirectory(); QUrl destinationDirectory = QUrl::fromLocalFile(extractJob->destinationDirectory()).adjusted(QUrl::NormalizePathSegments); qCDebug(ARK) << "Shall open URL" << destinationDirectory; KRun::runUrl(destinationDirectory, QStringLiteral("inode/directory"), widget()); } if (ArkSettings::closeAfterExtraction()) { emit quit(); } } } void Part::adjustColumns() { m_view->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); } -void Part::slotAddFiles(const QStringList& filesToAdd, const QString& path) +void Part::slotAddFiles(const QStringList& filesToAdd, const Archive::Entry *destination, const QString &relPath) { if (filesToAdd.isEmpty()) { return; } - qCDebug(ARK) << "Adding " << filesToAdd << " to " << path; + QStringList withChildPaths; + foreach (const QString& file, filesToAdd) { + m_jobTempEntries.push_back(new Archive::Entry(Q_NULLPTR, file)); + if (QFileInfo(file).isDir()) { + withChildPaths << file + QLatin1Char('/'); + QDirIterator it(file, QDir::AllEntries | QDir::Readable | QDir::Hidden | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); + while (it.hasNext()) { + QString path = it.next(); + if (it.fileInfo().isDir()) { + path += QLatin1Char('/'); + } + withChildPaths << path; + } + } + else { + withChildPaths << file; + } + } + qCDebug(ARK) << withChildPaths; + withChildPaths = m_model->entryPathsFromDestination(withChildPaths, destination, 0); + QList conflictingEntries; + bool error = m_model->conflictingEntries(conflictingEntries, withChildPaths, true); + + if (conflictingEntries.count() > 0) { + QPointer overwriteDialog = new OverwriteDialog(widget(), conflictingEntries, m_model->entryIcons(), error); + int ret = overwriteDialog->exec(); + delete overwriteDialog; + if (ret == QDialog::Rejected) { + qDeleteAll(m_jobTempEntries); + m_jobTempEntries.clear(); + return; + } + } // GlobalWorkDir is used by AddJob and should contain the part of the // absolute path of files to be added that should NOT be included in the // directory structure within the archive. // Example: We add file "/home/user/somedir/somefile.txt" and want the file // to have the relative path within the archive "somedir/somefile.txt". // GlobalWorkDir is then: "/home/user" QString globalWorkDir = filesToAdd.first(); // path represents the path of the file within the archive. This needs to // be removed from globalWorkDir, otherwise the files will be added to the // root of the archive. In the example above, path would be "somedir/". - if (!path.isEmpty()) { - globalWorkDir.remove(path); + if (!relPath.isEmpty()) { + globalWorkDir.remove(relPath); + qCDebug(ARK) << "Adding" << filesToAdd << "to" << relPath; + } + else { + qCDebug(ARK) << "Adding " << filesToAdd << ((destination == Q_NULLPTR) ? QString() : QStringLiteral("to ") + destination->fullPath()); } // Remove trailing slash (needed when adding dirs). if (globalWorkDir.right(1) == QLatin1String("/")) { globalWorkDir.chop(1); } CompressionOptions options(m_model->archive()->compressionOptions()); // Now take the absolute path of the parent directory. globalWorkDir = QFileInfo(globalWorkDir).dir().absolutePath(); qCDebug(ARK) << "Detected GlobalWorkDir to be " << globalWorkDir; options[QStringLiteral("GlobalWorkDir")] = globalWorkDir; - foreach (const QString& file, filesToAdd) { - m_jobTempEntries.push_back(new Archive::Entry(Q_NULLPTR, file)); - } - AddJob *job = m_model->addFiles(m_jobTempEntries, Q_NULLPTR, options); + AddJob *job = m_model->addFiles(m_jobTempEntries, destination, options); if (!job) { + qDeleteAll(m_jobTempEntries); + m_jobTempEntries.clear(); return; } connect(job, &KJob::result, this, &Part::slotAddFilesDone); registerJob(job); job->start(); } void Part::slotAddFiles() { - // If compression options are already set, we dont use the values from CreateDialog. + // If compression options are already set, we don't use the values from CreateDialog. CompressionOptions opts; if (m_model->archive()->compressionOptions().isEmpty()) { if (arguments().metaData().contains(QStringLiteral("compressionLevel"))) { opts[QStringLiteral("CompressionLevel")] = arguments().metaData()[QStringLiteral("compressionLevel")]; } m_model->archive()->setCompressionOptions(opts); } else { opts = m_model->archive()->compressionOptions(); } + QString dialogTitle = QStringLiteral("Add Files"); + const Archive::Entry *destination = Q_NULLPTR; + if (QObject::sender() == m_addFilesToAction) { + destination = m_model->entryForIndex(m_view->selectionModel()->currentIndex()); + dialogTitle += QStringLiteral(" to ") + destination->fullPath(); + } + qCDebug(ARK) << "Opening AddDialog with opts:" << opts; // #264819: passing widget() as the parent will not work as expected. // KFileDialog will create a KFileWidget, which runs an internal // event loop to stat the given directory. This, in turn, leads to // events being delivered to widget(), which is a QSplitter, which // in turn reimplements childEvent() and will end up calling // QWidget::show() on the KFileDialog (thus showing it in a // non-modal state). // When KFileDialog::exec() is called, the widget is already shown // and nothing happens. QPointer dlg = new AddDialog(widget(), - i18nc("@title:window", "Add Files"), + i18nc("@title:window", dialogTitle.toUtf8()), m_lastUsedAddPath, m_model->archive()->mimeType(), opts); if (dlg->exec() == QDialog::Accepted) { qCDebug(ARK) << "Selected files:" << dlg->selectedFiles(); qCDebug(ARK) << "Options:" << dlg->compressionOptions(); m_model->archive()->setCompressionOptions(dlg->compressionOptions()); - slotAddFiles(dlg->selectedFiles(), QString()); + slotAddFiles(dlg->selectedFiles(), destination, QString()); } delete dlg; } +void Part::slotEditFileName() +{ + QModelIndex currentIndex = m_view->selectionModel()->currentIndex(); + currentIndex = (currentIndex.parent().isValid()) + ? currentIndex.parent().child(currentIndex.row(), 0) + : m_model->index(currentIndex.row(), 0); + m_view->openEntryEditor(currentIndex); +} + +void Part::slotCutFiles() +{ + m_filesToMove = filesForIndexes(addChildren(m_view->selectionModel()->selectedRows())); + m_filesToCopy.clear(); + updateActions(); +} + +void Part::slotCopyFiles() +{ + m_filesToCopy = filesForIndexes(addChildren(m_view->selectionModel()->selectedRows())); + m_filesToMove.clear(); + updateActions(); +} + +void Part::slotRenameFile(QString name) +{ + if (name == QStringLiteral(".") || name == QStringLiteral("..") || name.contains(QLatin1Char('/'))) { + QMessageBox messageBox(QMessageBox::Warning, + i18n("Invalid filename"), + i18n("Filename can't contain slashes and can't be equal to \".\" or \"..\""), + QMessageBox::Ok); + messageBox.exec(); + return; + } + const Archive::Entry *entry = m_model->entryForIndex(m_view->selectionModel()->currentIndex()); + QList entriesToMove = filesForIndexes(addChildren(m_view->selectionModel()->selectedRows())); + + m_destination = new Archive::Entry(); + const QString &entryPath = entry->fullPath(true); + const QString rootPath = entryPath.left(entryPath.count() - entry->name().count()); + m_destination->setFullPath(rootPath + name + ((entry->isDir()) ? QLatin1Char('/') : QChar())); + + slotPasteFiles(entriesToMove, m_destination, 1); +} + +void Part::slotPasteFiles() +{ + m_destination = (m_view->selectionModel()->selectedRows().count() > 0) + ? m_model->entryForIndex(m_view->selectionModel()->currentIndex()) + : Q_NULLPTR; + if (m_destination == Q_NULLPTR) { + m_destination = new Archive::Entry(Q_NULLPTR, QString()); + } + else { + m_destination = new Archive::Entry(Q_NULLPTR, m_destination->fullPath()); + } + + if (m_filesToMove.count() > 0) { + // Changing destination to include new entry path if pasting only 1 entry. + QList entriesWithoutChildren = ReadOnlyArchiveInterface::entriesWithoutChildren(m_filesToMove); + if (entriesWithoutChildren.count() == 1) { + const Archive::Entry *entry = entriesWithoutChildren.first(); + const QString nameWithSlash = entry->name() + ((entry->isDir()) ? QLatin1String("/") : QString()); + m_destination->setFullPath(m_destination->fullPath() + nameWithSlash); + } + + foreach (const Archive::Entry *entry, entriesWithoutChildren) { + if (entry->isDir() && m_destination->fullPath().startsWith(entry->fullPath())) { + QMessageBox messageBox(QMessageBox::Warning, + i18n("Moving a folder into itself"), + i18n("Folders can't be moved into themselves."), + QMessageBox::Ok); + messageBox.exec(); + delete m_destination; + return; + } + } + slotPasteFiles(m_filesToMove, m_destination, entriesWithoutChildren.count()); + m_filesToMove.clear(); + } + else { + slotPasteFiles(m_filesToCopy, m_destination, 0); + m_filesToCopy.clear(); + } + updateActions(); +} + +void Part::slotPasteFiles(QList &files, Kerfuffle::Archive::Entry *destination, int entriesWithoutChildren) +{ + if (files.isEmpty()) { + delete m_destination; + return; + } + + QStringList filesPaths = ReadOnlyArchiveInterface::entryFullPaths(files); + QStringList newPaths = m_model->entryPathsFromDestination(filesPaths, destination, entriesWithoutChildren); + + if (ArchiveModel::hasDuplicatedEntries(newPaths)) { + QMessageBox messageBox(QMessageBox::Warning, + i18n("Pasting entries with the same name"), + i18n("Entries with the same names can't be pasted to the same destination."), + QMessageBox::Ok); + messageBox.exec(); + delete m_destination; + return; + } + + QList conflictingEntries; + bool error = m_model->conflictingEntries(conflictingEntries, newPaths, false); + + if (conflictingEntries.count() != 0) { + QPointer overwriteDialog = new OverwriteDialog(widget(), conflictingEntries, m_model->entryIcons(), error); + int ret = overwriteDialog->exec(); + delete overwriteDialog; + if (ret == QDialog::Rejected) { + delete m_destination; + return; + } + } + + if (entriesWithoutChildren > 0) { + qCDebug(ARK) << "Moving" << files << "to" << destination; + } + else { + qCDebug(ARK) << "Copying " << files << "to" << destination; + } + + CompressionOptions options(m_model->archive()->compressionOptions()); + + KJob *job; + if (entriesWithoutChildren != 0) { + job = m_model->moveFiles(files, destination, options); + } + else { + job = m_model->copyFiles(files, destination, options); + } + + if (job) { + connect(job, &KJob::result, + this, &Part::slotPasteFilesDone); + registerJob(job); + job->start(); + } + else { + delete m_destination; + } +} + void Part::slotAddFilesDone(KJob* job) { qDeleteAll(m_jobTempEntries); m_jobTempEntries.clear(); if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } else { // Hide the "archive will be created as soon as you add a file" message. m_messageWidget->hide(); } + m_filesToMove.clear(); + m_filesToCopy.clear(); +} + +void Part::slotPasteFilesDone(KJob *job) +{ + if (job->error() && job->error() != KJob::KilledJobError) { + KMessageBox::error(widget(), job->errorString()); + } + m_filesToMove.clear(); + m_filesToCopy.clear(); } void Part::slotDeleteFilesDone(KJob* job) { if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } + m_filesToMove.clear(); + m_filesToCopy.clear(); } void Part::slotDeleteFiles() { const int selectionsCount = m_view->selectionModel()->selectedRows().count(); const auto reallyDelete = KMessageBox::questionYesNo(widget(), i18ncp("@info", "Deleting this file is not undoable. Are you sure you want to do this?", "Deleting these files is not undoable. Are you sure you want to do this?", selectionsCount), i18ncp("@title:window", "Delete File", "Delete Files", selectionsCount), KStandardGuiItem::del(), KStandardGuiItem::no(), QString(), KMessageBox::Dangerous | KMessageBox::Notify); if (reallyDelete == KMessageBox::No) { return; } DeleteJob *job = m_model->deleteFiles(filesForIndexes(addChildren(m_view->selectionModel()->selectedRows()))); connect(job, &KJob::result, this, &Part::slotDeleteFilesDone); registerJob(job); job->start(); } void Part::slotShowProperties() { QPointer dialog(new Kerfuffle::PropertiesDialog(0, m_model->archive())); dialog.data()->show(); } void Part::slotToggleInfoPanel(bool visible) { if (visible) { m_splitter->setSizes(ArkSettings::splitterSizes()); m_infoPanel->show(); } else { // We need to save the splitterSizes before hiding, otherwise // Ark won't remember resizing done by the user. ArkSettings::setSplitterSizes(m_splitter->sizes()); m_infoPanel->hide(); } } void Part::slotSaveAs() { QUrl saveUrl = QFileDialog::getSaveFileUrl(widget(), i18nc("@title:window", "Save Archive As"), url()); if ((saveUrl.isValid()) && (!saveUrl.isEmpty())) { auto statJob = KIO::stat(saveUrl, KIO::StatJob::DestinationSide, 0); KJobWidgets::setWindow(statJob, widget()); if (statJob->exec()) { int overwrite = KMessageBox::warningContinueCancel(widget(), xi18nc("@info", "An archive named %1 already exists. Are you sure you want to overwrite it?", saveUrl.fileName()), QString(), KStandardGuiItem::overwrite()); if (overwrite != KMessageBox::Continue) { return; } } QUrl srcUrl = QUrl::fromLocalFile(localFilePath()); if (!QFile::exists(localFilePath())) { if (url().isLocalFile()) { KMessageBox::error(widget(), xi18nc("@info", "The archive %1 cannot be copied to the specified location. The archive does not exist anymore.", localFilePath())); return; } else { srcUrl = url(); } } KIO::Job *copyJob = KIO::file_copy(srcUrl, saveUrl, -1, KIO::Overwrite); KJobWidgets::setWindow(copyJob, widget()); copyJob->exec(); if (copyJob->error()) { KMessageBox::error(widget(), xi18nc("@info", "The archive could not be saved as %1. Try saving it to another location.", saveUrl.path())); } } } void Part::slotShowContextMenu() { if (!factory()) { return; } QMenu *popup = static_cast(factory()->container(QStringLiteral("context_menu"), this)); popup->popup(QCursor::pos()); } void Part::displayMsgWidget(KMessageWidget::MessageType type, const QString& msg) { // The widget could be already visible, so hide it. m_messageWidget->hide(); m_messageWidget->setText(msg); m_messageWidget->setMessageType(type); m_messageWidget->animatedShow(); } } // namespace Ark #include "part.moc" diff --git a/part/part.h b/part/part.h index 20ffcb32..becaf6a0 100644 --- a/part/part.h +++ b/part/part.h @@ -1,190 +1,220 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 Harald Hvaal * Copyright (C) 2009 Raphael Kubo da Costa * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #ifndef PART_H #define PART_H #include "interface.h" #include "kerfuffle/archiveentry.h" #include #include #include #include #include class ArchiveModel; +class ArchiveView; class InfoPanel; class KAboutData; class KAbstractWidgetJobTracker; class KJob; class KToggleAction; class QAction; class QSplitter; class QTreeView; class QTemporaryDir; class QVBoxLayout; class QSignalMapper; class QFileSystemWatcher; class QGroupBox; class QPlainTextEdit; namespace Ark { class Part: public KParts::ReadWritePart, public Interface { Q_OBJECT Q_INTERFACES(Interface) public: enum OpenFileMode { Preview, OpenFile, OpenFileWith }; Part(QWidget *parentWidget, QObject *parent, const QVariantList &); ~Part(); static KAboutData *createAboutData(); bool openFile() Q_DECL_OVERRIDE; bool saveFile() Q_DECL_OVERRIDE; bool isBusy() const Q_DECL_OVERRIDE; KConfigSkeleton *config() const Q_DECL_OVERRIDE; QList settingsPages(QWidget *parent) const Q_DECL_OVERRIDE; /** * Validate the localFilePath() associated to this part. * If the file is not valid, an error message is displayed to the user. * @return Whether the localFilePath() can be loaded by the part. */ bool isLocalFileValid(); /** * Ask the user whether to overwrite @p targetFile, when creating a new archive with the same path. * @return True if the file has been successfully removed upon user's will. False otherwise. */ bool confirmAndDelete(const QString& targetFile); public slots: void extractSelectedFilesTo(const QString& localPath); private slots: void slotLoadingStarted(); void slotLoadingFinished(KJob *job); void slotOpenExtractedEntry(KJob*); void slotPreviewExtractedEntry(KJob* job); void slotOpenEntry(int mode); void slotError(const QString& errorMessage, const QString& details); void slotExtractArchive(); void slotShowExtractionDialog(); void slotExtractionDone(KJob*); void slotQuickExtractFiles(QAction*); + + /** + * Creates and starts AddJob. + * + * @param files Files to add. + * @param destination Destination path within the archive to which entries have to be added. Is used on addto action + * or drag'n'drop event, for adding a watched file it has empty. + * @param relPath Relative path of watched entry inside the archive. Is used only for adding temporarily extracted + * watched file. + */ + void slotAddFiles(const QStringList &files, const Kerfuffle::Archive::Entry *destination, const QString &relPath); + + /** + * Creates and starts MoveJob or CopyJob. + * + * @param files Files to paste. + * @param destination Destination path within the archive to which entries have to be added. For renaming an entry + * the path has to contain a new filename too. + * @param entriesWithoutChildren Entries count, excluding their children. For CopyJob 0 MUST be passed. + */ + void slotPasteFiles(QList &files, Kerfuffle::Archive::Entry *destination, int entriesWithoutChildren); + void slotAddFiles(); - void slotAddFiles(const QStringList& files, const QString& path = QString()); + void slotEditFileName(); + void slotCutFiles(); + void slotCopyFiles(); + void slotRenameFile(QString name); + void slotPasteFiles(); void slotAddFilesDone(KJob*); + void slotPasteFilesDone(KJob*); void slotTestingDone(KJob*); void slotDeleteFiles(); void slotDeleteFilesDone(KJob*); void slotShowProperties(); void slotShowContextMenu(); void slotActivated(QModelIndex); void slotToggleInfoPanel(bool); void slotSaveAs(); void updateActions(); void updateQuickExtractMenu(QAction *extractAction); void selectionChanged(); void adjustColumns(); void setBusyGui(); void setReadyGui(); void setFileNameFromArchive(); void slotWatchedFileModified(const QString& file); void slotShowComment(); void slotAddComment(); void slotCommentChanged(); void slotTestArchive(); signals: void busy(); void ready(); void quit(); private: void resetGui(); void setupView(); void setupActions(); bool isSingleFolderArchive() const; QString detectSubfolder() const; QList filesForIndexes(const QModelIndexList& list) const; QList filesAndRootNodesForIndexes(const QModelIndexList& list) const; QModelIndexList addChildren(const QModelIndexList &list) const; void registerJob(KJob *job); void displayMsgWidget(KMessageWidget::MessageType type, const QString& msg); ArchiveModel *m_model; - QTreeView *m_view; + ArchiveView *m_view; QAction *m_previewAction; QAction *m_openFileAction; QAction *m_openFileWithAction; QAction *m_extractArchiveAction; QAction *m_extractAction; QAction *m_addFilesAction; QAction *m_addFilesToAction; QAction *m_renameFileAction; QAction *m_deleteFilesAction; QAction *m_cutFilesAction; QAction *m_copyFilesAction; QAction *m_pasteFilesAction; QAction *m_saveAsAction; QAction *m_propertiesAction; QAction *m_editCommentAction; QAction *m_testArchiveAction; KToggleAction *m_showInfoPanelAction; InfoPanel *m_infoPanel; QSplitter *m_splitter; QList m_tmpOpenDirList; bool m_busy; OpenFileMode m_openFileMode; QUrl m_lastUsedAddPath; QList m_jobTempEntries; - QList m_filesToMoveOrCopy; + QList m_filesToMove; + QList m_filesToCopy; + Kerfuffle::Archive::Entry *m_destination; KAbstractWidgetJobTracker *m_jobTracker; KParts::StatusBarExtension *m_statusBarExtension; QVBoxLayout *m_vlayout; QSignalMapper *m_signalMapper; QFileSystemWatcher *m_fileWatcher; QSplitter *m_commentSplitter; QGroupBox *m_commentBox; QPlainTextEdit *m_commentView; KMessageWidget *m_commentMsgWidget; KMessageWidget *m_messageWidget; }; } // namespace Ark #endif // PART_H diff --git a/plugins/libarchive/readwritelibarchiveplugin.cpp b/plugins/libarchive/readwritelibarchiveplugin.cpp index b176304d..95ac19e3 100644 --- a/plugins/libarchive/readwritelibarchiveplugin.cpp +++ b/plugins/libarchive/readwritelibarchiveplugin.cpp @@ -1,558 +1,563 @@ /* * 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 CompressionOptions &options) { qCDebug(ARK) << "Adding" << files.size() << "entries with CompressionOptions" << options; const bool creatingNewFile = !QFileInfo::exists(filename()); m_writtenFiles.clear(); if (!creatingNewFile && !initializeReader()) { return false; } if (!initializeWriter(creatingNewFile, options)) { return false; } // First write the new files. qCDebug(ARK) << "Writing new entries"; int no_entries = 0; // Recreate destination directory structure. const QString destinationPath = (destination == Q_NULLPTR) ? QString() : destination->fullPath(); foreach(Archive::Entry *selectedFile, files) { if (m_abortOperation) { break; } if (!writeFile(selectedFile->fullPath(), destinationPath)) { finish(false); return false; } no_entries++; // For directories, write all subfiles/folders. const QString &fullPath = selectedFile->fullPath(); 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)) { finish(false); return false; } no_entries++; } } } qCDebug(ARK) << "Added" << no_entries << "new entries to archive"; bool isSuccessful = true; // If we have old archive entries. if (!creatingNewFile) { qCDebug(ARK) << "Copying any old entries"; m_filePaths = m_writtenFiles; isSuccessful = processOldEntries(no_entries, Add); if (isSuccessful) { qCDebug(ARK) << "Added" << no_entries << "old entries to archive"; } else { qCDebug(ARK) << "Adding entries failed"; } } m_abortOperation = false; finish(isSuccessful); return isSuccessful; } bool ReadWriteLibarchivePlugin::moveFiles(const QList &files, Archive::Entry *destination, const CompressionOptions &options) { Q_UNUSED(options); qCDebug(ARK) << "Moving" << files.size() << "entries"; if (!initializeReader()) { return false; } if (!initializeWriter()) { return false; } // Copy old elements from previous archive to new archive. int no_entries = 0; m_filePaths = entryFullPaths(entriesWithoutChildren(files)); m_destination = destination; const bool isSuccessful = processOldEntries(no_entries, Move); if (isSuccessful) { qCDebug(ARK) << "Moved" << no_entries << "entries within archive"; } else { qCDebug(ARK) << "Moving entries failed"; } finish(isSuccessful); return isSuccessful; } bool ReadWriteLibarchivePlugin::copyFiles(const QList &files, Archive::Entry *destination, const CompressionOptions &options) { Q_UNUSED(options); qCDebug(ARK) << "Copying" << files.size() << "entries"; if (!initializeReader()) { return false; } if (!initializeWriter()) { return false; } // Copy old elements from previous archive to new archive. int no_entries = 0; m_filePaths = entryFullPaths(entriesWithoutChildren(files)); m_destination = destination; const bool isSuccessful = processOldEntries(no_entries, Copy); if (isSuccessful) { qCDebug(ARK) << "Copied" << no_entries << "entries within archive"; } else { qCDebug(ARK) << "Copying entries failed"; } finish(isSuccessful); return isSuccessful; } bool ReadWriteLibarchivePlugin::deleteFiles(const QList &files) { qCDebug(ARK) << "Deleting" << files.size() << "entries"; if (!initializeReader()) { return false; } if (!initializeWriter()) { return false; } // Copy old elements from previous archive to new archive. int no_entries = 0; m_filePaths = entryFullPaths(files); const bool isSuccessful = processOldEntries(no_entries, Delete); if (isSuccessful) { qCDebug(ARK) << "Removed" << no_entries << "entries from archive"; } else { qCDebug(ARK) << "Removing entries failed"; } finish(isSuccessful); return isSuccessful; } bool ReadWriteLibarchivePlugin::initializeWriter(const bool creatingNewFile, const CompressionOptions &options) { // |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). m_tempFile.setFileName(filename()); if (!m_tempFile.open(QIODevice::WriteOnly | QIODevice::Unbuffered)) { emit error(xi18nc("@info", "Failed to create a temporary file to compress %1.", filename())); return false; } m_archiveWriter.reset(archive_write_new()); if (!(m_archiveWriter.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(m_archiveWriter.data()); if (creatingNewFile) { if (!initializeNewFileWriterFilters(options)) { return false; } } else { if (!initializeWriterFilters()) { return false; } } if (archive_write_open_fd(m_archiveWriter.data(), m_tempFile.handle()) != ARCHIVE_OK) { emit error(xi18nc("@info", "Opening the archive for writing failed with the following error:" "%1", QLatin1String(archive_error_string(m_archiveWriter.data())))); return false; } return true; } bool ReadWriteLibarchivePlugin::initializeWriterFilters() { int ret; bool requiresExecutable = false; switch (archive_filter_code(m_archiveReader.data(), 0)) { case ARCHIVE_FILTER_GZIP: ret = archive_write_add_filter_gzip(m_archiveWriter.data()); break; case ARCHIVE_FILTER_BZIP2: ret = archive_write_add_filter_bzip2(m_archiveWriter.data()); break; case ARCHIVE_FILTER_XZ: ret = archive_write_add_filter_xz(m_archiveWriter.data()); break; case ARCHIVE_FILTER_LZMA: ret = archive_write_add_filter_lzma(m_archiveWriter.data()); break; case ARCHIVE_FILTER_COMPRESS: ret = archive_write_add_filter_compress(m_archiveWriter.data()); break; case ARCHIVE_FILTER_LZIP: ret = archive_write_add_filter_lzip(m_archiveWriter.data()); break; case ARCHIVE_FILTER_LZOP: ret = archive_write_add_filter_lzop(m_archiveWriter.data()); break; case ARCHIVE_FILTER_LRZIP: ret = archive_write_add_filter_lrzip(m_archiveWriter.data()); requiresExecutable = true; break; #ifdef HAVE_LIBARCHIVE_3_2_0 case ARCHIVE_FILTER_LZ4: ret = archive_write_add_filter_lz4(m_archiveWriter.data()); break; #endif case ARCHIVE_FILTER_NONE: ret = archive_write_add_filter_none(m_archiveWriter.data()); break; default: emit error(i18n("The compression type '%1' is not supported by Ark.", QLatin1String(archive_filter_name(m_archiveReader.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(m_archiveWriter.data())))); return false; } return true; } bool ReadWriteLibarchivePlugin::initializeNewFileWriterFilters(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(m_archiveWriter.data()); } else if (filename().right(3).toUpper() == QLatin1String("BZ2")) { qCDebug(ARK) << "Detected bzip2 compression for new file"; ret = archive_write_add_filter_bzip2(m_archiveWriter.data()); } else if (filename().right(2).toUpper() == QLatin1String("XZ")) { qCDebug(ARK) << "Detected xz compression for new file"; ret = archive_write_add_filter_xz(m_archiveWriter.data()); } else if (filename().right(4).toUpper() == QLatin1String("LZMA")) { qCDebug(ARK) << "Detected lzma compression for new file"; ret = archive_write_add_filter_lzma(m_archiveWriter.data()); } else if (filename().right(2).toUpper() == QLatin1String(".Z")) { qCDebug(ARK) << "Detected compress (.Z) compression for new file"; ret = archive_write_add_filter_compress(m_archiveWriter.data()); } else if (filename().right(2).toUpper() == QLatin1String("LZ")) { qCDebug(ARK) << "Detected lzip compression for new file"; ret = archive_write_add_filter_lzip(m_archiveWriter.data()); } else if (filename().right(3).toUpper() == QLatin1String("LZO")) { qCDebug(ARK) << "Detected lzop compression for new file"; ret = archive_write_add_filter_lzop(m_archiveWriter.data()); } else if (filename().right(3).toUpper() == QLatin1String("LRZ")) { qCDebug(ARK) << "Detected lrzip compression for new file"; ret = archive_write_add_filter_lrzip(m_archiveWriter.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(m_archiveWriter.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(m_archiveWriter.data()); } else { qCDebug(ARK) << "Falling back to gzip"; ret = archive_write_add_filter_gzip(m_archiveWriter.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(m_archiveWriter.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(m_archiveWriter.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(m_archiveWriter.data())))); return false; } } return true; } void ReadWriteLibarchivePlugin::finish(const bool isSuccessful) { if (!isSuccessful) { m_tempFile.cancelWriting(); } archive_write_close(m_archiveWriter.data()); m_tempFile.commit(); } bool ReadWriteLibarchivePlugin::processOldEntries(int &entriesCounter, OperationMode mode) { struct archive_entry *entry; m_lastMovedFolder = QString(); entriesCounter = 0; // If destination path doesn't contain a target entry name, we have to remember to include it // while moving or copying folder contents. int nameLength = 0; while ((mode != Add || !m_abortOperation) && archive_read_next_header(m_archiveReader.data(), &entry) == ARCHIVE_OK) { const QString file = QFile::decodeName(archive_entry_pathname(entry)); if (mode == Move || mode == Copy) { QString newPathname; bool found = true; if (m_lastMovedFolder.count() > 0 && file.startsWith(m_lastMovedFolder)) { // Replace last moved or copied folder path with destination path. int charsCount = file.count() - m_lastMovedFolder.count(); if (mode == Copy || m_filePaths.count() > 1) { charsCount += nameLength; } newPathname = m_destination->fullPath() + file.right(charsCount); } else if (m_filePaths.contains(file)) { const QString name = file.split(QLatin1Char('/'), QString::SkipEmptyParts).last(); if (mode == Copy || m_filePaths.count() > 1) { newPathname = m_destination->fullPath() + name; } else { // If the mode is set to Move and there is only one passed file in the list, // we have to use destination as newPathname. newPathname = m_destination->fullPath(); } if (file.right(1) == QLatin1String("/")) { nameLength = name.count() + 1; // plus slash m_lastMovedFolder = file; } else { nameLength = 0; // plus slash m_lastMovedFolder = QString(); } } else { found = false; m_lastMovedFolder = QString(); } if (found) { if (mode == Copy) { - writeEntry(entry); + if (!writeEntry(entry)) { + return false; + } } else { emit entryRemoved(file); } entriesCounter++; archive_entry_set_pathname(entry, newPathname.toUtf8()); } } else if (m_filePaths.contains(file)) { archive_read_data_skip(m_archiveReader.data()); switch (mode) { case Delete: entriesCounter++; emit entryRemoved(file); break; case Add: qCDebug(ARK) << file << "is already present in the new archive, skipping."; break; default: qCDebug(ARK) << "Mode" << mode << "is not considered for processing old libarchive entries"; Q_ASSERT(false); } continue; } if (writeEntry(entry)) { if (mode == Add) { entriesCounter++; } + else if (mode == Move || mode == Copy) { + emitEntryFromArchiveEntry(entry); + } } else { return false; } } return true; } bool ReadWriteLibarchivePlugin::writeEntry(struct archive_entry *entry) { const int returnCode = archive_write_header(m_archiveWriter.data(), entry); const QString file = QFile::decodeName(archive_entry_pathname(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)), m_archiveReader.data(), m_archiveWriter.data(), false); break; case ARCHIVE_FAILED: case ARCHIVE_FATAL: qCCritical(ARK) << "archive_write_header() has returned" << returnCode << "with errno" << archive_errno(m_archiveWriter.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; } 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) { 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(m_archiveWriter.data(), entry)) == ARCHIVE_OK) { // If the whole archive is extracted and the total filesize is // available, we use partial progress. copyData(absoluteFilename, m_archiveWriter.data(), false); } else { qCCritical(ARK) << "Writing header failed with error code " << header_response; qCCritical(ARK) << "Error while writing..." << archive_error_string(m_archiveWriter.data()) << "(error no =" << archive_errno(m_archiveWriter.data()) << ')'; emit error(xi18nc("@info Error in a message box", "Ark could not compress %1:%2", absoluteFilename, QString::fromUtf8(archive_error_string(m_archiveWriter.data())))); archive_entry_free(entry); return false; } m_writtenFiles.push_back(destinationFilename); emitEntryFromArchiveEntry(entry); archive_entry_free(entry); return true; } #include "readwritelibarchiveplugin.moc"