diff --git a/autotests/kerfuffle/jsonarchiveinterface.cpp b/autotests/kerfuffle/jsonarchiveinterface.cpp index a6d74a03..6ce548be 100644 --- a/autotests/kerfuffle/jsonarchiveinterface.cpp +++ b/autotests/kerfuffle/jsonarchiveinterface.cpp @@ -1,106 +1,110 @@ /* * Copyright (c) 2010-2011 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "jsonarchiveinterface.h" #include JSONArchiveInterface::JSONArchiveInterface(QObject *parent, const QVariantList& args) : Kerfuffle::ReadWriteArchiveInterface(parent, args) { } JSONArchiveInterface::~JSONArchiveInterface() { } bool JSONArchiveInterface::list() { JSONParser::JSONArchive::const_iterator it = m_archive.constBegin(); for (; it != m_archive.constEnd(); ++it) { emit entry(*it); } return true; } bool JSONArchiveInterface::open() { QFile file(filename()); if (!file.exists()) { return false; } if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { return false; } m_archive = JSONParser::parse(&file); return !m_archive.isEmpty(); } bool JSONArchiveInterface::addFiles(const QStringList& files, const Kerfuffle::CompressionOptions& options) { Q_UNUSED(options) foreach (const QString& file, files) { if (m_archive.contains(file)) { return false; } Kerfuffle::ArchiveEntry e; e[Kerfuffle::FileName] = file; m_archive[file] = e; } return true; } bool JSONArchiveInterface::copyFiles(const QList& files, const QString& destinationDirectory, const Kerfuffle::ExtractionOptions& options) { Q_UNUSED(files) Q_UNUSED(destinationDirectory) Q_UNUSED(options) return true; } bool JSONArchiveInterface::deleteFiles(const QList& files) { foreach (const QVariant& file, files) { const QString fileName = file.toString(); if (m_archive.contains(fileName)) { m_archive.remove(fileName); emit entryRemoved(fileName); } } return true; } - +bool JSONArchiveInterface::addComment(const QString& comment) +{ + Q_UNUSED(comment) + return true; +} diff --git a/autotests/kerfuffle/jsonarchiveinterface.h b/autotests/kerfuffle/jsonarchiveinterface.h index 86a57a25..b7d9f88c 100644 --- a/autotests/kerfuffle/jsonarchiveinterface.h +++ b/autotests/kerfuffle/jsonarchiveinterface.h @@ -1,66 +1,67 @@ /* * Copyright (c) 2010-2011 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef JSONARCHIVEINTERFACE_H #define JSONARCHIVEINTERFACE_H #include "jsonparser.h" #include "kerfuffle/archiveinterface.h" #include "kerfuffle/archive_kerfuffle.h" /** * A dummy archive interface used by our test cases. * * It reads a JSON file which defines the contents of the archive. * For the file format description, see the documentation for @c JSONParser. * * The file's content is read to memory when open() is called and the archive * is then closed. This means that this class never changes the file's content * on disk, and entry addition or deletion do not change the original file. * * @sa JSONParser * * @author Raphael Kubo da Costa */ class JSONArchiveInterface : public Kerfuffle::ReadWriteArchiveInterface { Q_OBJECT public: explicit JSONArchiveInterface(QObject *parent, const QVariantList& args); virtual ~JSONArchiveInterface(); virtual bool list() Q_DECL_OVERRIDE; virtual bool open() Q_DECL_OVERRIDE; virtual bool addFiles(const QStringList& files, const Kerfuffle::CompressionOptions& options) Q_DECL_OVERRIDE; virtual bool copyFiles(const QList& files, const QString& destinationDirectory, const Kerfuffle::ExtractionOptions& options) Q_DECL_OVERRIDE; virtual bool deleteFiles(const QList& files) Q_DECL_OVERRIDE; + virtual bool addComment(const QString& comment) Q_DECL_OVERRIDE; private: JSONParser::JSONArchive m_archive; }; #endif diff --git a/kerfuffle/archive_kerfuffle.cpp b/kerfuffle/archive_kerfuffle.cpp index cc4bf07f..9e38044e 100644 --- a/kerfuffle/archive_kerfuffle.cpp +++ b/kerfuffle/archive_kerfuffle.cpp @@ -1,401 +1,413 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008 Harald Hvaal * Copyright (c) 2009-2011 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "archive_kerfuffle.h" #include "ark_debug.h" #include "archiveinterface.h" #include "jobs.h" #include "mimetypes.h" #include "pluginmanager.h" #include #include #include #include #include #include #include #include namespace Kerfuffle { QDebug operator<<(QDebug d, const fileRootNodePair &pair) { d.nospace() << "fileRootNodePair(" << pair.file << "," << pair.rootNode << ")"; return d.space(); } Archive *Archive::create(const QString &fileName, QObject *parent) { return create(fileName, QString(), parent); } Archive *Archive::create(const QString &fileName, const QString &fixedMimeType, QObject *parent) { qCDebug(ARK) << "Going to create archive" << fileName; qRegisterMetaType("ArchiveEntry"); PluginManager pluginManager; const QMimeType mimeType = fixedMimeType.isEmpty() ? determineMimeType(fileName) : QMimeDatabase().mimeTypeForName(fixedMimeType); const QVector offers = pluginManager.preferredPluginsFor(mimeType); if (offers.isEmpty()) { qCCritical(ARK) << "Could not find a plugin to handle" << fileName; return new Archive(NoPlugin, parent); } Archive *archive; foreach (Plugin *plugin, offers) { archive = create(fileName, plugin, parent); // Use the first valid plugin, according to the priority sorting. if (archive->isValid()) { return archive; } } qCCritical(ARK) << "Failed to find a usable plugin for" << fileName; return archive; } Archive *Archive::create(const QString &fileName, Plugin *plugin, QObject *parent) { Q_ASSERT(plugin); qCDebug(ARK) << "Checking plugin" << plugin->metaData().pluginId(); KPluginFactory *factory = KPluginLoader(plugin->metaData().fileName()).factory(); if (!factory) { qCWarning(ARK) << "Invalid plugin factory for" << plugin->metaData().pluginId(); return new Archive(FailedPlugin, parent); } const QVariantList args = {QVariant(QFileInfo(fileName).absoluteFilePath())}; ReadOnlyArchiveInterface *iface = factory->create(Q_NULLPTR, args); if (!iface) { qCWarning(ARK) << "Could not create plugin instance" << plugin->metaData().pluginId(); return new Archive(FailedPlugin, parent); } if (!plugin->isValid()) { qCDebug(ARK) << "Cannot use plugin" << plugin->metaData().pluginId() << "- check whether" << plugin->readOnlyExecutables() << "are installed."; return new Archive(FailedPlugin, parent); } qCDebug(ARK) << "Successfully loaded plugin" << plugin->metaData().pluginId(); return new Archive(iface, !plugin->isReadWrite(), parent); } Archive::Archive(ArchiveError errorCode, QObject *parent) : QObject(parent) , m_iface(Q_NULLPTR) , m_error(errorCode) { qCDebug(ARK) << "Created archive instance with error"; } Archive::Archive(ReadOnlyArchiveInterface *archiveInterface, bool isReadOnly, QObject *parent) : QObject(parent) , m_iface(archiveInterface) , m_hasBeenListed(false) , m_isReadOnly(isReadOnly) , m_isSingleFolderArchive(false) , m_extractedFilesSize(0) , m_error(NoError) , m_encryptionType(Unencrypted) , m_numberOfFiles(0) { qCDebug(ARK) << "Created archive instance"; Q_ASSERT(archiveInterface); archiveInterface->setParent(this); QMetaType::registerComparators(); QMetaType::registerDebugStreamOperator(); connect(m_iface, &ReadOnlyArchiveInterface::entry, this, &Archive::onNewEntry); } Archive::~Archive() { } QString Archive::completeBaseName() const { QString base = QFileInfo(fileName()).completeBaseName(); // Special case for compressed tar archives. if (base.right(4).toUpper() == QLatin1String(".TAR")) { base.chop(4); } return base; } QString Archive::fileName() const { return isValid() ? m_iface->filename() : QString(); } QString Archive::comment() const { return isValid() ? m_iface->comment() : QString(); } +CommentJob* Archive::addComment(const QString &comment) +{ + if (!isValid()) { + return Q_NULLPTR; + } + + qCDebug(ARK) << "Going to add comment:" << comment; + Q_ASSERT(!isReadOnly()); + CommentJob *job = new CommentJob(comment, static_cast(m_iface), this); + return job; +} + QMimeType Archive::mimeType() const { return isValid() ? determineMimeType(fileName()) : QMimeType(); } bool Archive::isReadOnly() const { return isValid() ? (m_iface->isReadOnly() || m_isReadOnly) : false; } bool Archive::isSingleFolderArchive() { if (!isValid()) { return false; } listIfNotListed(); return m_isSingleFolderArchive; } bool Archive::hasComment() const { return isValid() ? !comment().isEmpty() : false; } Archive::EncryptionType Archive::encryptionType() { if (!isValid()) { return Unencrypted; } listIfNotListed(); return m_encryptionType; } qulonglong Archive::numberOfFiles() { if (!isValid()) { return 0; } listIfNotListed(); return m_numberOfFiles; } qulonglong Archive::unpackedSize() { if (!isValid()) { return 0; } listIfNotListed(); return m_extractedFilesSize; } qulonglong Archive::packedSize() const { return isValid() ? QFileInfo(fileName()).size() : 0; } QString Archive::subfolderName() { if (!isValid()) { return QString(); } listIfNotListed(); return m_subfolderName; } void Archive::onNewEntry(const ArchiveEntry &entry) { if (!entry[IsDirectory].toBool()) { m_numberOfFiles++; } } bool Archive::isValid() const { return m_iface && (m_error == NoError); } ArchiveError Archive::error() const { return m_error; } KJob* Archive::open() { return 0; } KJob* Archive::create() { return 0; } ListJob* Archive::list() { if (!isValid() || !QFileInfo::exists(fileName())) { return Q_NULLPTR; } qCDebug(ARK) << "Going to list files"; ListJob *job = new ListJob(m_iface, this); //if this job has not been listed before, we grab the opportunity to //collect some information about the archive if (!m_hasBeenListed) { connect(job, &ListJob::result, this, &Archive::onListFinished); } return job; } DeleteJob* Archive::deleteFiles(const QList & files) { if (!isValid()) { return Q_NULLPTR; } qCDebug(ARK) << "Going to delete files" << files; if (m_iface->isReadOnly()) { return 0; } DeleteJob *newJob = new DeleteJob(files, static_cast(m_iface), this); return newJob; } AddJob* Archive::addFiles(const QStringList & files, const CompressionOptions& options) { if (!isValid()) { return Q_NULLPTR; } CompressionOptions newOptions = options; if (encryptionType() != Unencrypted) { newOptions[QStringLiteral("PasswordProtectedHint")] = true; } qCDebug(ARK) << "Going to add files" << files << "with options" << newOptions; Q_ASSERT(!m_iface->isReadOnly()); AddJob *newJob = new AddJob(files, newOptions, static_cast(m_iface), this); connect(newJob, &AddJob::result, this, &Archive::onAddFinished); return newJob; } ExtractJob* Archive::copyFiles(const QList& files, const QString& destinationDir, const ExtractionOptions& options) { if (!isValid()) { return Q_NULLPTR; } ExtractionOptions newOptions = options; if (encryptionType() != Unencrypted) { newOptions[QStringLiteral( "PasswordProtectedHint" )] = true; } ExtractJob *newJob = new ExtractJob(files, destinationDir, newOptions, m_iface, this); return newJob; } void Archive::encrypt(const QString &password, bool encryptHeader) { if (!isValid()) { return; } m_iface->setPassword(password); m_iface->setHeaderEncryptionEnabled(encryptHeader); m_encryptionType = encryptHeader ? HeaderEncrypted : Encrypted; } void Archive::onAddFinished(KJob* job) { //if the archive was previously a single folder archive and an add job //has successfully finished, then it is no longer a single folder //archive (for the current implementation, which does not allow adding //folders/files other places than the root. //TODO: handle the case of creating a new file and singlefolderarchive //then. if (m_isSingleFolderArchive && !job->error()) { m_isSingleFolderArchive = false; } } void Archive::onListFinished(KJob* job) { ListJob *ljob = qobject_cast(job); m_extractedFilesSize = ljob->extractedFilesSize(); m_isSingleFolderArchive = ljob->isSingleFolderArchive(); m_subfolderName = ljob->subfolderName(); if (m_subfolderName.isEmpty()) { m_subfolderName = completeBaseName(); } if (ljob->isPasswordProtected()) { // If we already know the password, it means that the archive is header-encrypted. m_encryptionType = m_iface->password().isEmpty() ? Encrypted : HeaderEncrypted; } m_hasBeenListed = true; } void Archive::listIfNotListed() { if (!m_hasBeenListed) { ListJob *job = list(); if (!job) { return; } connect(job, &ListJob::userQuery, this, &Archive::onUserQuery); QEventLoop loop(this); connect(job, &KJob::result, &loop, &QEventLoop::quit); job->start(); loop.exec(); // krazy:exclude=crashy } } void Archive::onUserQuery(Query* query) { query->execute(); } } // namespace Kerfuffle diff --git a/kerfuffle/archive_kerfuffle.h b/kerfuffle/archive_kerfuffle.h index 36acef8c..6ec4454f 100644 --- a/kerfuffle/archive_kerfuffle.h +++ b/kerfuffle/archive_kerfuffle.h @@ -1,252 +1,254 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008 Harald Hvaal * Copyright (c) 2011 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef ARCHIVE_H #define ARCHIVE_H #include "kerfuffle_export.h" #include #include #include #include #include class KJob; namespace Kerfuffle { class ListJob; class ExtractJob; class DeleteJob; class AddJob; +class CommentJob; class Plugin; class Query; class ReadOnlyArchiveInterface; /** * 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. */ enum EntryMetaDataType { FileName = 0, /**< The entry's file name */ InternalID, /**< The entry's ID for Ark's internal manipulation */ Permissions, /**< The entry's permissions */ Owner, /**< The user the entry belongs to */ Group, /**< The user group the entry belongs to */ Size, /**< The entry's original size */ CompressedSize, /**< The compressed size for the entry */ Link, /**< The entry is a symbolic link */ 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 */ IsDirectory, /**< The entry is a directory */ Comment, IsPasswordProtected, /**< The entry is password-protected */ Custom = 1048576 }; enum ArchiveError { NoError = 0, NoPlugin, FailedPlugin }; typedef QHash ArchiveEntry; /** These are the extra options for doing the compression. Naming convention is CamelCase with either Global, or the compression type (such as Zip, Rar, etc), followed by the property name used */ typedef QHash CompressionOptions; typedef QHash ExtractionOptions; /** * Stores a filename and rootnode pair. This is used to cut an individual * rootnode from the path of each file, e.g. when drag'n'drop extracting a * selection of files. */ struct fileRootNodePair { QString file; QString rootNode; fileRootNodePair() {} fileRootNodePair(const QString &f) : file(f) {} fileRootNodePair(const QString &f, const QString &n) : file(f), rootNode(n) {} // Required to compare QVariants with this type. bool operator==(const fileRootNodePair &right) const { if (file == right.file) return true; else return false; } bool operator<(const fileRootNodePair &) const { return false; } }; QDebug operator<<(QDebug d, const fileRootNodePair &pair); class KERFUFFLE_EXPORT Archive : public QObject { Q_OBJECT Q_ENUMS(EncryptionType) /** * Complete base name, without the "tar" extension (if any). */ Q_PROPERTY(QString completeBaseName READ completeBaseName CONSTANT) Q_PROPERTY(QString fileName READ fileName CONSTANT) Q_PROPERTY(QString comment READ comment CONSTANT) Q_PROPERTY(QMimeType mimeType READ mimeType CONSTANT) Q_PROPERTY(bool isReadOnly READ isReadOnly CONSTANT) Q_PROPERTY(bool isSingleFolderArchive READ isSingleFolderArchive) Q_PROPERTY(EncryptionType encryptionType READ encryptionType) Q_PROPERTY(qulonglong numberOfFiles READ numberOfFiles) Q_PROPERTY(qulonglong unpackedSize READ unpackedSize) Q_PROPERTY(qulonglong packedSize READ packedSize) Q_PROPERTY(QString subfolderName READ subfolderName) public: enum EncryptionType { Unencrypted, Encrypted, HeaderEncrypted }; QString completeBaseName() const; QString fileName() const; QString comment() const; QMimeType mimeType() const; bool isReadOnly() const; bool isSingleFolderArchive(); bool hasComment() const; EncryptionType encryptionType(); qulonglong numberOfFiles(); qulonglong unpackedSize(); qulonglong packedSize() const; QString subfolderName(); static Archive *create(const QString &fileName, QObject *parent = 0); static Archive *create(const QString &fileName, const QString &fixedMimeType, QObject *parent = 0); /** * Create an archive instance from a given @p plugin. * @param fileName The name of the archive. * @return A valid archive if the plugin could be loaded, an invalid one otherwise (with the FailedPlugin error set). */ static Archive *create(const QString &fileName, Plugin *plugin, QObject *parent = Q_NULLPTR); ~Archive(); ArchiveError error() const; bool isValid() const; KJob* open(); KJob* create(); /** * @return A ListJob if the archive already exists. A null pointer otherwise. */ ListJob* list(); DeleteJob* deleteFiles(const QList & files); + CommentJob* addComment(const QString &comment); /** * Compression options that should be handled by all interfaces: * * GlobalWorkDir - Change to this dir before adding the new files. * The path names should then be added relative to this directory. * * TODO: find a way to actually add files to specific locations in * the archive * (not supported yet) GlobalPathInArchive - a path relative to the * archive root where the files will be added under * */ AddJob* addFiles(const QStringList & files, const CompressionOptions& options = CompressionOptions()); ExtractJob* copyFiles(const QList &files, const QString &destinationDir, const ExtractionOptions &options = ExtractionOptions()); /** * @param password The password to encrypt the archive with. * @param encryptHeader Whether to encrypt also the list of files. */ void encrypt(const QString &password, bool encryptHeader); private slots: void onListFinished(KJob*); void onAddFinished(KJob*); void onUserQuery(Kerfuffle::Query*); void onNewEntry(const ArchiveEntry &entry); private: Archive(ReadOnlyArchiveInterface *archiveInterface, bool isReadOnly, QObject *parent = 0); Archive(ArchiveError errorCode, QObject *parent = 0); void listIfNotListed(); ReadOnlyArchiveInterface *m_iface; bool m_hasBeenListed; bool m_isReadOnly; bool m_isSingleFolderArchive; QString m_subfolderName; qulonglong m_extractedFilesSize; ArchiveError m_error; EncryptionType m_encryptionType; qulonglong m_numberOfFiles; }; } // namespace Kerfuffle Q_DECLARE_METATYPE(Kerfuffle::Archive::EncryptionType) Q_DECLARE_METATYPE(Kerfuffle::fileRootNodePair) #endif // ARCHIVE_H diff --git a/kerfuffle/archiveformat.cpp b/kerfuffle/archiveformat.cpp index 1d207eab..cea08583 100644 --- a/kerfuffle/archiveformat.cpp +++ b/kerfuffle/archiveformat.cpp @@ -1,101 +1,110 @@ /* * Copyright (c) 2016 Elvis Angelaccio * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "archiveformat.h" namespace Kerfuffle { ArchiveFormat::ArchiveFormat() : m_encryptionType(Archive::Unencrypted) { } ArchiveFormat::ArchiveFormat(const QMimeType& mimeType, Archive::EncryptionType encryptionType, int minCompLevel, int maxCompLevel, - int defaultCompLevel) : + int defaultCompLevel, + bool supportsWriteComment) : m_mimeType(mimeType), m_encryptionType(encryptionType), m_minCompressionLevel(minCompLevel), m_maxCompressionLevel(maxCompLevel), - m_defaultCompressionLevel(defaultCompLevel) + m_defaultCompressionLevel(defaultCompLevel), + m_supportsWriteComment(supportsWriteComment) { } ArchiveFormat ArchiveFormat::fromMetadata(const QMimeType& mimeType, const KPluginMetaData& metadata) { const QJsonObject json = metadata.rawData(); foreach (const QString& mime, metadata.mimeTypes()) { if (mimeType.name() != mime) { continue; } const QJsonObject formatProps = json[mime].toObject(); int minCompLevel = formatProps[QStringLiteral("CompressionLevelMin")].toInt(); int maxCompLevel = formatProps[QStringLiteral("CompressionLevelMax")].toInt(); int defaultCompLevel = formatProps[QStringLiteral("CompressionLevelDefault")].toInt(); + bool supportsWriteComment = formatProps[QStringLiteral("SupportsWriteComment")].toBool(); + Archive::EncryptionType encType = Archive::Unencrypted; if (formatProps[QStringLiteral("HeaderEncryption")].toBool()) { encType = Archive::HeaderEncrypted; } else if (formatProps[QStringLiteral("Encryption")].toBool()) { encType = Archive::Encrypted; } - return ArchiveFormat(mimeType, encType, minCompLevel, maxCompLevel, defaultCompLevel); + return ArchiveFormat(mimeType, encType, minCompLevel, maxCompLevel, defaultCompLevel, supportsWriteComment); } return ArchiveFormat(); } bool ArchiveFormat::isValid() const { return m_mimeType.isValid(); } Archive::EncryptionType ArchiveFormat::encryptionType() const { return m_encryptionType; } int ArchiveFormat::minCompressionLevel() const { return m_minCompressionLevel; } int ArchiveFormat::maxCompressionLevel() const { return m_maxCompressionLevel; } int ArchiveFormat::defaultCompressionLevel() const { return m_defaultCompressionLevel; } +bool ArchiveFormat::supportsWriteComment() const +{ + return m_supportsWriteComment; +} + } diff --git a/kerfuffle/archiveformat.h b/kerfuffle/archiveformat.h index fb7587f7..856590da 100644 --- a/kerfuffle/archiveformat.h +++ b/kerfuffle/archiveformat.h @@ -1,75 +1,78 @@ /* * Copyright (c) 2016 Elvis Angelaccio * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef ARCHIVEFORMAT_H #define ARCHIVEFORMAT_H #include "archive_kerfuffle.h" #include namespace Kerfuffle { class KERFUFFLE_EXPORT ArchiveFormat { public: explicit ArchiveFormat(); explicit ArchiveFormat(const QMimeType& mimeType, Kerfuffle::Archive::EncryptionType encryptionType, int minCompLevel, int maxCompLevel, - int defaultCompLevel); + int defaultCompLevel, + bool supportsWriteComment); /** * @return The archive format of the given @p mimeType, according to the given @p metadata. */ static ArchiveFormat fromMetadata(const QMimeType& mimeType, const KPluginMetaData& metadata); /** * @return Whether the format is associated to a valid mimetype. */ bool isValid() const; /** * @return The encryption type supported by the archive format. */ Kerfuffle::Archive::EncryptionType encryptionType() const; int minCompressionLevel() const; int maxCompressionLevel() const; int defaultCompressionLevel() const; + bool supportsWriteComment() const; private: QMimeType m_mimeType; Kerfuffle::Archive::EncryptionType m_encryptionType; int m_minCompressionLevel; int m_maxCompressionLevel; int m_defaultCompressionLevel; + bool m_supportsWriteComment; }; } #endif // ARCHIVEFORMAT_H diff --git a/kerfuffle/archiveinterface.h b/kerfuffle/archiveinterface.h index 141c117d..37c449ef 100644 --- a/kerfuffle/archiveinterface.h +++ b/kerfuffle/archiveinterface.h @@ -1,155 +1,156 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef ARCHIVEINTERFACE_H #define ARCHIVEINTERFACE_H #include "archive_kerfuffle.h" #include "kerfuffle_export.h" #include #include #include #include namespace Kerfuffle { class Query; class KERFUFFLE_EXPORT ReadOnlyArchiveInterface: public QObject { Q_OBJECT public: explicit ReadOnlyArchiveInterface(QObject *parent, const QVariantList & args); virtual ~ReadOnlyArchiveInterface(); /** * Returns the filename of the archive currently being handled. */ QString filename() const; /** * Returns the comment of the archive. */ QString comment() const; /** * @return The password of the archive, if any. */ QString password() const; /** * Returns whether the file can only be read. * * @return @c true The file cannot be written. * @return @c false The file can be read and written. */ virtual bool isReadOnly() const; virtual bool open(); /** * List archive contents. * This runs the process of reading archive contents. * When subclassing, you can block as long as you need (unless you called setWaitForFinishedSignal(true)). * @returns whether the listing succeeded. * @note If returning false, make sure to emit the error() signal beforewards to notify * the user of the error condition. */ virtual bool list() = 0; void setPassword(const QString &password); void setHeaderEncryptionEnabled(bool enabled); /** * Extract files from archive. * Globally recognized extraction options: * @li PreservePaths - preserve file paths (extract flat if false) * @li RootNode - node in the archive which will correspond to the @arg destinationDirectory * When subclassing, you can block as long as you need (unless you called setWaitForFinishedSignal(true)). * @returns whether the listing succeeded. * @note If returning false, make sure to emit the error() signal beforewards to notify * the user of the error condition. */ virtual bool copyFiles(const QList &files, const QString &destinationDirectory, const ExtractionOptions &options) = 0; bool waitForFinishedSignal(); virtual bool doKill(); virtual bool doSuspend(); virtual bool doResume(); virtual bool isCliBased() const; bool isHeaderEncryptionEnabled() const; signals: void cancelled(); void error(const QString &message, const QString &details = QString()); void entry(const ArchiveEntry &archiveEntry); void entryRemoved(const QString &path); void progress(double progress); void info(const QString &info); void finished(bool result); void userQuery(Query *query); protected: /** * Setting this option to true will not run the functions in their own thread. * Instead it will be necessary to call finished(bool) when the operation is actually finished. */ void setWaitForFinishedSignal(bool value); void setCorrupt(bool isCorrupt); bool isCorrupt() const; QString m_comment; private: QString m_filename; QString m_password; bool m_waitForFinishedSignal; bool m_isHeaderEncryptionEnabled; bool m_isCorrupt; }; class KERFUFFLE_EXPORT ReadWriteArchiveInterface: public ReadOnlyArchiveInterface { Q_OBJECT public: explicit ReadWriteArchiveInterface(QObject *parent, const QVariantList & args); virtual ~ReadWriteArchiveInterface(); bool isReadOnly() const Q_DECL_OVERRIDE; //see archive.h for a list of what the compressionoptions might //contain virtual bool addFiles(const QStringList & files, const CompressionOptions& options) = 0; virtual bool deleteFiles(const QList & files) = 0; + virtual bool addComment(const QString &comment) = 0; }; } // namespace Kerfuffle #endif // ARCHIVEINTERFACE_H diff --git a/kerfuffle/cliinterface.cpp b/kerfuffle/cliinterface.cpp index 1acc63fd..c2d18dcd 100644 --- a/kerfuffle/cliinterface.cpp +++ b/kerfuffle/cliinterface.cpp @@ -1,1143 +1,1203 @@ /* * 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_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; } bool CliInterface::isCliBased() const { return true; } void CliInterface::setListEmptyLines(bool emptyLines) { m_listEmptyLines = emptyLines; } bool CliInterface::list() { resetParsing(); cacheParameterList(); m_operationMode = List; const auto args = substituteListVariables(m_param.value(ListArgs).toStringList(), password()); if (!runProcess(m_param.value(ListProgram).toStringList(), args)) { failOperation(); return false; } return true; } bool CliInterface::copyFiles(const QVariantList &files, const QString &destinationDirectory, const ExtractionOptions &options) { qCDebug(ARK) << Q_FUNC_INFO << "to" << destinationDirectory; cacheParameterList(); m_operationMode = Copy; m_compressionOptions = options; m_copiedFiles = files; m_extractDestDir = destinationDirectory; const QStringList extractArgs = m_param.value(ExtractArgs).toStringList(); if (extractArgs.contains(QStringLiteral("$PasswordSwitch")) && options.value(QStringLiteral("PasswordProtectedHint")).toBool() && password().isEmpty()) { qCDebug(ARK) << "Password hint enabled, querying user"; if (!passwordQuery()) { return false; } } // Populate the argument list. const QStringList args = substituteCopyVariables(extractArgs, files, options.value(QStringLiteral("PreservePaths")).toBool(), password(), options.value(QStringLiteral("RootNode"), QString()).toString()); 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."; failOperation(); 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)) { failOperation(); return false; } return true; } bool CliInterface::addFiles(const QStringList & files, const CompressionOptions& options) { cacheParameterList(); m_operationMode = Add; const QStringList addArgs = m_param.value(AddArgs).toStringList(); 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(), files, password(), isHeaderEncryptionEnabled(), compLevel); if (!runProcess(m_param.value(AddProgram).toStringList(), args)) { failOperation(); return false; } return true; } bool CliInterface::deleteFiles(const QList & files) { cacheParameterList(); m_operationMode = Delete; //start preparing the argument list QStringList args = m_param.value(DeleteArgs).toStringList(); //now replace the various elements in the list for (int i = 0; i < args.size(); ++i) { QString argument = args.at(i); qCDebug(ARK) << "Processing argument " << argument; if (argument == QLatin1String( "$Archive" )) { args[i] = filename(); } else if (argument == QLatin1String( "$Files" )) { args.removeAt(i); for (int j = 0; j < files.count(); ++j) { args.insert(i + j, escapeFileName(files.at(j).toString())); ++i; } --i; } } m_removedFiles = files; if (!runProcess(m_param.value(DeleteProgram).toStringList(), args)) { failOperation(); return false; } return true; } bool CliInterface::runProcess(const QStringList& programNames, const QStringList& arguments) { Q_ASSERT(!m_process); QString programPath; for (int i = 0; i < programNames.count(); i++) { programPath = QStandardPaths::findExecutable(programNames.at(i)); if (!programPath.isEmpty()) break; } if (programPath.isEmpty()) { const QString names = programNames.join(QStringLiteral(", ")); emit error(xi18ncp("@info", "Failed to locate program %2 on disk.", "Failed to locate programs %2 on disk.", programNames.count(), names)); emit finished(false); return false; } qCDebug(ARK) << "Executing" << programPath << arguments << "within directory" << QDir::currentPath(); #ifdef Q_OS_WIN m_process = new KProcess; #else m_process = new KPtyProcess; m_process->setPtyChannels(KPtyProcess::StdinChannel); #endif m_process->setOutputChannelMode(KProcess::MergedChannels); m_process->setNextOpenMode(QIODevice::ReadWrite | QIODevice::Unbuffered | QIODevice::Text); m_process->setProgram(programPath, arguments); connect(m_process, SIGNAL(readyReadStandardOutput()), SLOT(readStdout()), Qt::DirectConnection); if (m_operationMode == Copy) { // Extraction jobs need a dedicated post-processing function. connect(m_process, static_cast(&KPtyProcess::finished), this, &CliInterface::copyProcessFinished, Qt::DirectConnection); } else { connect(m_process, static_cast(&KPtyProcess::finished), this, &CliInterface::processFinished, Qt::DirectConnection); } m_stdOutData.clear(); m_process->start(); return true; } void CliInterface::processFinished(int exitCode, QProcess::ExitStatus exitStatus) { m_exitCode = exitCode; qCDebug(ARK) << "Process finished, exitcode:" << exitCode << "exitstatus:" << exitStatus; if (m_process) { //handle all the remaining data in the process readStdout(true); delete m_process; m_process = Q_NULLPTR; } // #193908 - #222392 // Don't emit finished() if the job was killed quietly. if (m_abortingOperation) { return; } if (m_operationMode == Delete) { foreach(const QVariant& v, m_removedFiles) { emit entryRemoved(v.toString()); } } emit progress(1.0); if (m_operationMode == Add) { list(); } else { emit finished(true); } } void CliInterface::copyProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) { Q_ASSERT(m_operationMode == Copy); m_exitCode = exitCode; qCDebug(ARK) << "Extraction process finished, exitcode:" << exitCode << "exitstatus:" << exitStatus; if (m_process) { // Handle all the remaining data in the process. readStdout(true); delete m_process; m_process = Q_NULLPTR; } if (m_compressionOptions.value(QStringLiteral("AlwaysUseTmpDir")).toBool()) { // unar exits with code 1 if extraction fails. // This happens at least with wrong passwords or not enough space in the destination folder. if (m_exitCode == 1) { if (password().isEmpty()) { qCWarning(ARK) << "Extraction aborted, destination folder might not have enough space."; emit error(i18n("Extraction failed. Make sure that enough space is available.")); } else { qCWarning(ARK) << "Extraction aborted, either the password is wrong or the destination folder doesn't have enough space."; emit error(i18n("Extraction failed. Make sure you provided the correct password and that enough space is available.")); setPassword(QString()); } copyProcessCleanup(); emit finished(false); return; } if (!m_compressionOptions.value(QStringLiteral("DragAndDrop")).toBool()) { if (!moveToDestination(QDir::current(), QDir(m_extractDestDir), m_compressionOptions[QStringLiteral("PreservePaths")].toBool())) { emit error(i18ncp("@info", "Could not move the extracted file to the destination directory.", "Could not move the extracted files to the destination directory.", m_copiedFiles.size())); copyProcessCleanup(); emit finished(false); return; } copyProcessCleanup(); } } if (m_compressionOptions.value(QStringLiteral("DragAndDrop")).toBool()) { if (!moveDroppedFilesToDest(m_copiedFiles, m_extractDestDir)) { emit error(i18ncp("@info", "Could not move the extracted file to the destination directory.", "Could not move the extracted files to the destination directory.", m_copiedFiles.size())); copyProcessCleanup(); emit finished(false); return; } copyProcessCleanup(); } emit progress(1.0); emit finished(true); } bool CliInterface::moveDroppedFilesToDest(const QVariantList &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 QVariant& file, files) { QFileInfo relEntry(file.value().file.remove(file.value().rootNode)); QFileInfo absSourceEntry(QDir::current().absolutePath() + QLatin1Char('/') + file.value().file); QFileInfo absDestEntry(finalDestDir.path() + QLatin1Char('/') + relEntry.filePath()); if (absSourceEntry.isDir()) { // For directories, just create the path. if (!finalDestDir.mkpath(relEntry.filePath())) { qCWarning(ARK) << "Failed to create directory" << relEntry.filePath() << "in final destination."; } } else { // If destination file exists, prompt the user. if (absDestEntry.exists()) { qCWarning(ARK) << "File" << absDestEntry.absoluteFilePath() << "exists."; if (!skipAll && !overwriteAll) { Kerfuffle::OverwriteQuery query(absDestEntry.absoluteFilePath()); query.setNoRenameMode(true); emit userQuery(&query); query.waitForResponse(); if (query.responseOverwrite() || query.responseOverwriteAll()) { if (query.responseOverwriteAll()) { overwriteAll = true; } if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } else if (query.responseSkip() || query.responseAutoSkip()) { if (query.responseAutoSkip()) { skipAll = true; } continue; } else if (query.responseCancelled()) { qCDebug(ARK) << "Copy action cancelled."; return false; } } else if (skipAll) { continue; } else if (overwriteAll) { if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } } // Create any parent directories. if (!finalDestDir.mkpath(relEntry.path())) { qCWarning(ARK) << "Failed to create parent directory for file:" << absDestEntry.filePath(); } // Move files to the final destination. if (!QFile(absSourceEntry.absoluteFilePath()).rename(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to move file" << absSourceEntry.filePath() << "to final destination."; return false; } } } return true; } bool CliInterface::isEmptyDir(const QDir &dir) { QDir d = dir; d.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot); return d.count() == 0; } void CliInterface::copyProcessCleanup() { if (!m_oldWorkingDir.isEmpty()) { QDir::setCurrent(m_oldWorkingDir); } if (m_extractTempDir) { delete m_extractTempDir; m_extractTempDir = Q_NULLPTR; } } bool CliInterface::moveToDestination(const QDir &tempDir, const QDir &destDir, bool preservePaths) { qCDebug(ARK) << "Moving extracted files from temp dir" << tempDir.path() << "to final destination" << destDir.path(); bool overwriteAll = false; bool skipAll = false; QDirIterator dirIt(tempDir.path(), QDir::AllEntries | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (dirIt.hasNext()) { dirIt.next(); // We skip directories if: // 1. We are not preserving paths // 2. The dir is not empty. Only empty directories need to be explicitly moved. // The non-empty ones are created by QDir::mkpath() below. if (dirIt.fileInfo().isDir()) { if (!preservePaths || !isEmptyDir(QDir(dirIt.filePath()))) { continue; } } QFileInfo relEntry; if (preservePaths) { relEntry = QFileInfo(dirIt.filePath().remove(tempDir.path() + QLatin1Char('/'))); } else { relEntry = QFileInfo(dirIt.fileName()); } QFileInfo absDestEntry(destDir.path() + QLatin1Char('/') + relEntry.filePath()); if (absDestEntry.exists()) { qCWarning(ARK) << "File" << absDestEntry.absoluteFilePath() << "exists."; Kerfuffle::OverwriteQuery query(absDestEntry.absoluteFilePath()); query.setNoRenameMode(true); emit userQuery(&query); query.waitForResponse(); if (query.responseOverwrite() || query.responseOverwriteAll()) { if (query.responseOverwriteAll()) { overwriteAll = true; } if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } else if (query.responseSkip() || query.responseAutoSkip()) { if (query.responseAutoSkip()) { skipAll = true; } continue; } else if (query.responseCancelled()) { qCDebug(ARK) << "Copy action cancelled."; return false; } } else if (skipAll) { continue; } else if (overwriteAll) { if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } if (preservePaths) { // Create any parent directories. if (!destDir.mkpath(relEntry.path())) { qCWarning(ARK) << "Failed to create parent directory for file:" << absDestEntry.filePath(); } } // Move file to the final destination. if (!QFile(dirIt.filePath()).rename(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to move file" << dirIt.filePath() << "to final destination."; return false; } } return true; } QStringList CliInterface::substituteListVariables(const QStringList &listArgs, const QString &password) { // Required if we call this function from unit tests. cacheParameterList(); QStringList args; foreach (const QString& arg, listArgs) { if (arg == QLatin1String("$Archive")) { args << filename(); continue; } if (arg == QLatin1String("$PasswordSwitch")) { args << passwordSwitch(password); continue; } // Simple argument (e.g. -slt in 7z), nothing to substitute, just add it to the list. args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } QStringList CliInterface::substituteCopyVariables(const QStringList &extractArgs, const QVariantList &files, bool preservePaths, const QString &password, const QString &rootNode) { // 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("$RootNodeSwitch")) { args << rootNodeSwitch(rootNode); continue; } if (arg == QLatin1String("$Files")) { args << copyFilesList(files); 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 QStringList &files, 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 << files; 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::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; +} + 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::rootNodeSwitch(const QString &rootNode) const { if (rootNode.isEmpty()) { return QStringList(); } Q_ASSERT(m_param.contains(RootNodeSwitch)); QStringList rootNodeSwitch = m_param.value(RootNodeSwitch).toStringList(); Q_ASSERT(!rootNodeSwitch.isEmpty() && rootNodeSwitch.size() <= 2); if (rootNodeSwitch.size() == 1) { rootNodeSwitch[0].replace(QLatin1String("$Path"), rootNode); } else { rootNodeSwitch[1] = rootNode; } return rootNodeSwitch; } QStringList CliInterface::copyFilesList(const QVariantList& files) const { QStringList filesList; foreach (const QVariant& f, files) { filesList << escapeFileName(f.value().file); } return filesList; } void CliInterface::failOperation() { // TODO: Would be good to unit test #304764/#304178. doKill(); } 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); failOperation(); return false; } setPassword(query.password()); return true; } void CliInterface::readStdout(bool handleAll) { //when hacking this function, please remember the following: //- standard output comes in unpredictable chunks, this is why //you can never know if the last part of the output is a complete line or not //- console applications are not really consistent about what //characters they send out (newline, backspace, carriage return, //etc), so keep in mind that this function is supposed to handle //all those special cases and be the lowest common denominator if (m_abortingOperation) return; Q_ASSERT(m_process); if (!m_process->bytesAvailable()) { //if process has no more data, we can just bail out return; } QByteArray dd = m_process->readAllStandardOutput(); m_stdOutData += dd; QList lines = m_stdOutData.split('\n'); //The reason for this check is that archivers often do not end //queries (such as file exists, wrong password) on a new line, but //freeze waiting for input. So we check for errors on the last line in //all cases. // TODO: QLatin1String() might not be the best choice here. // The call to handleLine() at the end of the method uses // QString::fromLocal8Bit(), for example. // TODO: The same check methods are called in handleLine(), this // is suboptimal. bool wrongPasswordMessage = checkForErrorMessage(QLatin1String( lines.last() ), WrongPasswordPatterns); bool foundErrorMessage = (wrongPasswordMessage || checkForErrorMessage(QLatin1String(lines.last()), DiskFullPatterns) || checkForErrorMessage(QLatin1String(lines.last()), ExtractionFailedPatterns) || checkForPasswordPromptMessage(QLatin1String(lines.last())) || checkForErrorMessage(QLatin1String(lines.last()), FileExistsExpression)); if (foundErrorMessage) { handleAll = true; } if (wrongPasswordMessage) { setPassword(QString()); } //this is complex, here's an explanation: //if there is no newline, then there is no guaranteed full line to //handle in the output. The exception is that it is supposed to handle //all the data, OR if there's been an error message found in the //partial data. if (lines.size() == 1 && !handleAll) { return; } if (handleAll) { m_stdOutData.clear(); } else { //because the last line might be incomplete we leave it for now //note, this last line may be an empty string if the stdoutdata ends //with a newline m_stdOutData = lines.takeLast(); } foreach(const QByteArray& line, lines) { if (!line.isEmpty() || (m_listEmptyLines && m_operationMode == List)) { handleLine(QString::fromLocal8Bit(line)); } } } void CliInterface::handleLine(const QString& line) { // TODO: This should be implemented by each plugin; the way progress is // shown by each CLI application is subject to a lot of variation. if ((m_operationMode == Copy || m_operationMode == Add) && m_param.contains(CaptureProgress) && m_param.value(CaptureProgress).toBool()) { //read the percentage int pos = line.indexOf(QLatin1Char( '%' )); if (pos != -1 && pos > 1) { int percentage = line.midRef(pos - 2, 2).toInt(); emit progress(float(percentage) / 100); return; } } if (m_operationMode == Copy) { if (checkForPasswordPromptMessage(line)) { qCDebug(ARK) << "Found a password prompt"; Kerfuffle::PasswordNeededQuery query(filename()); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); failOperation(); 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.")); failOperation(); return; } if (checkForErrorMessage(line, WrongPasswordPatterns)) { qCWarning(ARK) << "Wrong password!"; setPassword(QString()); emit error(i18nc("@info", "Extraction failed: Incorrect password")); failOperation(); return; } if (checkForErrorMessage(line, ExtractionFailedPatterns)) { qCWarning(ARK) << "Error in extraction:" << line; emit error(i18n("Extraction failed because of an unexpected error.")); failOperation(); 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(); failOperation(); 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.")); failOperation(); return; } if (checkForErrorMessage(line, ExtractionFailedPatterns)) { qCWarning(ARK) << "Error in extraction!!"; emit error(i18n("Extraction failed because of an unexpected error.")); failOperation(); return; } if (checkForErrorMessage(line, CorruptArchivePatterns)) { qCWarning(ARK) << "Archive corrupt"; setCorrupt(true); Kerfuffle::LoadCorruptQuery query(filename()); emit userQuery(&query); query.waitForResponse(); if (!query.responseYes()) { emit cancelled(); failOperation(); return; } } if (handleFileExistsMessage(line)) { return; } readListLine(line); 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::doKill() { if (m_process) { // Give some time for the application to finish gracefully m_abortingOperation = true; 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; return true; } return false; } bool CliInterface::doSuspend() { return false; } bool CliInterface::doResume() { return false; } QString CliInterface::escapeFileName(const QString& fileName) const { return fileName; } void CliInterface::writeToProcess(const QByteArray& data) { Q_ASSERT(m_process); Q_ASSERT(!data.isNull()); qCDebug(ARK) << "Writing" << data << "to the process"; #ifdef Q_OS_WIN m_process->write(data); #else m_process->pty()->write(data); #endif } -} +bool CliInterface::addComment(const QString &comment) +{ + cacheParameterList(); + m_operationMode = Comment; + + m_commentTempFile = new QTemporaryFile; + if (!m_commentTempFile->open()) { + qCWarning(ARK) << "Failed to create temporary file for comment"; + failOperation(); + 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)) { + failOperation(); + return false; + } + m_comment = comment; + return true; +} + +} diff --git a/kerfuffle/cliinterface.h b/kerfuffle/cliinterface.h index 7e9d0aed..d83681cf 100644 --- a/kerfuffle/cliinterface.h +++ b/kerfuffle/cliinterface.h @@ -1,454 +1,474 @@ /* * 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 "kerfuffle_export.h" #include #include class KProcess; class KPtyProcess; class QDir; class QTemporaryDir; +class QTemporaryFile; namespace Kerfuffle { enum CliInterfaceParameters { ///////////////[ COMMON ]///////////// /** * Bool (default false) * Will look for the %-sign in the stdout while working, in the form of * (2%, 14%, 35%, etc etc), and report progress based upon this */ CaptureProgress = 0, /** * QString * Default: empty * A regexp pattern that matches the program's password prompt. */ PasswordPromptPattern, ///////////////[ LIST ]///////////// /** * QStringList * The names to the program that will handle listing of this * archive (eg "rar"). Will be searched for in PATH */ ListProgram, /** * QStringList * The arguments that are passed to the program above for * listing the archive. Special strings that will be * substituted: * $Archive - the path of the archive */ ListArgs, /** * QStringList (default empty) * List of regexp patterns that indicate a corrupt archive. */ CorruptArchivePatterns, ///////////////[ EXTRACT ]///////////// /** * QStringList * The names to the program that will handle extracting of this * archive (eg "rar"). Will be searched for in PATH */ ExtractProgram, /** * QStringList * The arguments that are passed to the program above for * extracting the archive. Special strings that will be * substituted: * $Archive - the path of the archive * $Files - the files selected to be extracted, if any * $PreservePathSwitch - the flag for extracting with full paths * $RootNodeSwitch - the internal work dir in the archive (for example * when the user has dragged a folder from the archive and wants it * extracted relative to it) * $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 $Path will be * substituted for the path string. * Example: ("--internalPath=$Path) * or ("--path", "$Path") */ RootNodeSwitch, /** * QStringList (default empty) * The format of the root node switch. The variable $Password will be * substituted for the password string. NOTE: supplying passwords * through a virtual terminal is not supported (yet?), because this * is not cross platform compatible. As of KDE 4.3 there are no plans to * change this. * Example: ("-p$Password) * or ("--password", "$Password") */ PasswordSwitch, /** * QString * The format of the compression level switch. The variable $CompressionLevel * will be substituted for the level. * Example: ("-mx=$CompressionLevel) */ CompressionLevelSwitch, /** * QStringList * This is a stringlist with regexps, defining how to recognize the last * line in a "File already exists" prompt when extracting. */ FileExistsExpression, /** * QStringList * This is a stringlist with regexps defining how to recognize the line * containing the filename in a "File already exists" prompt when * extracting. It should have one captured string, which is the filename * of the file/folder that already exists. */ FileExistsFileName, /** * int * This sets on what output channel the FileExistsExpression regex * should be applied on, in other words, on what stream the "file * exists" output will appear in. Values accepted: * 0 - Standard error, stderr (default) * 1 - Standard output, stdout */ FileExistsMode, /** * QStringList * The various responses that can be supplied as a response to the * "file exists" prompt. The various items are to be supplied in the * following order: * index 0 - Yes (overwrite) * index 1 - No (skip/do not overwrite) * index 2 - All (overwrite all) * index 3 - Do not overwrite any files (autoskip) * index 4 - Cancel operation */ FileExistsInput, /** * QStringList * Regexp patterns capturing disk is full error messages. */ DiskFullPatterns, ///////////////[ DELETE ]///////////// /** * QStringList * The names to the program that will handle deleting of elements in this * archive format (eg "rar"). Will be searched for in PATH */ DeleteProgram, /** * QStringList * The arguments that are passed to the program above for * deleting from the archive. Special strings that will be * substituted: * $Archive - the path of the archive * $Files - the files selected to be deleted */ DeleteArgs, /** * QStringList * Default: empty * A list of regexp patterns that will cause the extraction to exit * with a general fail message */ ExtractionFailedPatterns, /** * QStringList * Default: empty * A list of regexp patterns that will alert the user that the password * was wrong. */ WrongPasswordPatterns, ///////////////[ ADD ]///////////// /** * QStringList * The names to the program that will handle adding in this * archive format (eg "rar"). Will be searched for in PATH */ AddProgram, /** * QStringList * The arguments that are passed to the program above for * adding to the archive. Special strings that will be * substituted: * $Archive - the path of the archive * $Files - the files selected to be added */ AddArgs, ///////////////[ ENCRYPT ]///////////// /** * QStringList (default empty) * The variable $Password will be * substituted for the password string used to encrypt the header. * Example (rar plugin): ("-hp$Password") */ PasswordHeaderSwitch, + + ///////////////[ COMMENT ]///////////// + + /** + * QStringList + * The arguments that are passed to AddProgram when adding + * a comment. + */ + CommentArgs, + /** + * QString + * The variable $CommentFile will be substituted for the file + * containing the comment. + * Example (rar plugin): -z$CommentFile + */ + CommentSwitch }; typedef QHash ParameterList; class KERFUFFLE_EXPORT CliInterface : public ReadWriteArchiveInterface { Q_OBJECT public: enum OperationMode { - List, Copy, Add, Delete + List, Copy, Add, Delete, Comment }; OperationMode m_operationMode; explicit CliInterface(QObject *parent, const QVariantList & args); virtual ~CliInterface(); virtual bool list() Q_DECL_OVERRIDE; virtual bool copyFiles(const QList& files, const QString& destinationDirectory, const ExtractionOptions& options) Q_DECL_OVERRIDE; virtual bool addFiles(const QStringList & files, 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 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; bool isCliBased() const Q_DECL_OVERRIDE; /** * Returns the list of characters which are preceded by a * backslash when a file name in an archive is passed to * a program. * * @see setEscapedCharacters(). */ QString escapedCharacters(); // FIXME not implemented? /** * Sets which characters will be preceded by a backslash when * a file name in an archive is passed to a program. * * @see escapedCharacters(). */ void setEscapedCharacters(const QString& characters); // FIXME not implemented? /** * Sets if the listing should include empty lines. * * The default value is false. */ void setListEmptyLines(bool emptyLines); /** * Move all files from @p tmpDir to @p destDir, preserving paths if @p preservePaths is true. * @return Whether the operation has been successful. */ bool moveToDestination(const QDir &tempDir, const QDir &destDir, bool preservePaths); QStringList substituteListVariables(const QStringList &listArgs, const QString &password); QStringList substituteCopyVariables(const QStringList &extractArgs, const QVariantList &files, bool preservePaths, const QString &password, const QString &rootNode); QStringList substituteAddVariables(const QStringList &addArgs, const QStringList &files, const QString &password, bool encryptHeader, int compLevel); + QStringList substituteCommentVariables(const QStringList &commentArgs, const QString &commentFile); /** * @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 root node switch with the given @p rootNode. */ QStringList rootNodeSwitch(const QString& rootNode) const; /** * @return The list of selected files to extract. */ QStringList copyFilesList(const QVariantList& files) const; protected: virtual void handleLine(const QString& line); virtual void cacheParameterList(); /** * Run @p programName with the given @p arguments. * * @param programName The program that will be run (not the whole path). * @param arguments A list of arguments that will be passed to the program. * * @return @c true if the program was found and the process was started correctly, * @c false otherwise (in which case finished(false) is emitted). */ bool runProcess(const QStringList& programNames, const QStringList& arguments); void failOperation(); /** * Ask the password *before* running any process. * @return True if the user supplies a password, false otherwise (in which case finished() is emitted). */ bool passwordQuery(); ParameterList m_param; int m_exitCode; protected slots: virtual void readStdout(bool handleAll = false); private: /** * Checks whether a line of the program's output is a password prompt. * * It uses the regular expression in the @c PasswordPromptPattern parameter * for the check. * * @param line A line of the program's output. * * @return @c true if the given @p line is a password prompt, @c false * otherwise. */ bool checkForPasswordPromptMessage(const QString& line); bool handleFileExistsMessage(const QString& filename); bool checkForErrorMessage(const QString& line, int parameterIndex); /** * Performs any additional escaping and processing on @p fileName * before passing it to the underlying process. * * The default implementation returns @p fileName unchanged. * * @param fileName String to escape. */ virtual QString escapeFileName(const QString &fileName) const; /** * Wrapper around KProcess::write() or KPtyDevice::write(), depending on * the platform. */ void writeToProcess(const QByteArray& data); bool moveDroppedFilesToDest(const QVariantList &files, const QString &finalDest); /** * @return Whether @p dir is an empty directory. */ bool isEmptyDir(const QDir &dir); void copyProcessCleanup(); QByteArray m_stdOutData; QRegularExpression m_passwordPromptPattern; QHash > m_patternCache; #ifdef Q_OS_WIN KProcess *m_process; #else KPtyProcess *m_process; #endif QVariantList m_removedFiles; bool m_listEmptyLines; bool m_abortingOperation; QString m_storedFileName; CompressionOptions m_compressionOptions; QString m_oldWorkingDir; QString m_extractDestDir; QTemporaryDir *m_extractTempDir; + QTemporaryFile *m_commentTempFile; QVariantList m_copiedFiles; private slots: void processFinished(int exitCode, QProcess::ExitStatus exitStatus); void copyProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); }; } #endif /* CLIINTERFACE_H */ diff --git a/kerfuffle/jobs.cpp b/kerfuffle/jobs.cpp index 748085dc..b4d14af6 100644 --- a/kerfuffle/jobs.cpp +++ b/kerfuffle/jobs.cpp @@ -1,422 +1,445 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "jobs.h" #include "ark_debug.h" #include #include #include #include #include #include //#define DEBUG_RACECONDITION namespace Kerfuffle { class Job::Private : public QThread { public: Private(Job *job, QObject *parent = 0) : QThread(parent) , q(job) { connect(q, &KJob::result, this, &QThread::quit); } virtual void run() Q_DECL_OVERRIDE; private: Job *q; }; void Job::Private::run() { q->doWork(); if (q->isRunning()) { exec(); } #ifdef DEBUG_RACECONDITION QThread::sleep(2); #endif } Job::Job(ReadOnlyArchiveInterface *interface, QObject *parent) : KJob(parent) , m_archiveInterface(interface) , m_isRunning(false) , d(new Private(this)) { static bool onlyOnce = false; if (!onlyOnce) { qRegisterMetaType >("QPair"); onlyOnce = true; } setCapabilities(KJob::Killable); } Job::~Job() { if (d->isRunning()) { d->wait(); } delete d; } ReadOnlyArchiveInterface *Job::archiveInterface() { return m_archiveInterface; } bool Job::isRunning() const { return m_isRunning; } void Job::start() { jobTimer.start(); m_isRunning = true; if (archiveInterface()->isCliBased()) { // CLI-based interfaces run a QProcess, no need to use threads. QTimer::singleShot(0, this, &Job::doWork); } else { // Run the job in another thread. d->start(); } } void Job::emitResult() { m_isRunning = false; KJob::emitResult(); } void Job::connectToArchiveInterfaceSignals() { connect(archiveInterface(), &ReadOnlyArchiveInterface::cancelled, this, &Job::onCancelled); connect(archiveInterface(), &ReadOnlyArchiveInterface::error, this, &Job::onError); connect(archiveInterface(), &ReadOnlyArchiveInterface::entry, this, &Job::onEntry); connect(archiveInterface(), &ReadOnlyArchiveInterface::entryRemoved, this, &Job::onEntryRemoved); connect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &Job::onProgress); connect(archiveInterface(), &ReadOnlyArchiveInterface::info, this, &Job::onInfo); connect(archiveInterface(), &ReadOnlyArchiveInterface::finished, this, &Job::onFinished, Qt::DirectConnection); connect(archiveInterface(), &ReadOnlyArchiveInterface::userQuery, this, &Job::onUserQuery); } void Job::onCancelled() { qCDebug(ARK) << "Cancelled emitted"; setError(KJob::KilledJobError); } void Job::onError(const QString & message, const QString & details) { Q_UNUSED(details) qCDebug(ARK) << "Error emitted:" << message; setError(KJob::UserDefinedError); setErrorText(message); } void Job::onEntry(const ArchiveEntry & archiveEntry) { emit newEntry(archiveEntry); } void Job::onProgress(double value) { setPercent(static_cast(100.0*value)); } void Job::onInfo(const QString& info) { emit infoMessage(this, info); } void Job::onEntryRemoved(const QString & path) { emit entryRemoved(path); } void Job::onFinished(bool result) { qCDebug(ARK) << "Job finished, result:" << result << ", time:" << jobTimer.elapsed() << "ms"; emitResult(); } void Job::onUserQuery(Query *query) { emit userQuery(query); } bool Job::doKill() { bool ret = archiveInterface()->doKill(); if (!ret) { qCWarning(ARK) << "Killing does not seem to be supported here."; } return ret; } ListJob::ListJob(ReadOnlyArchiveInterface *interface, QObject *parent) : Job(interface, parent) , m_isSingleFolderArchive(true) , m_isPasswordProtected(false) , m_extractedFilesSize(0) , m_dirCount(0) , m_filesCount(0) { qCDebug(ARK) << "ListJob started"; connect(this, &ListJob::newEntry, this, &ListJob::onNewEntry); } void ListJob::doWork() { emit description(this, i18n("Loading archive...")); connectToArchiveInterfaceSignals(); bool ret = archiveInterface()->list(); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } qlonglong ListJob::extractedFilesSize() const { return m_extractedFilesSize; } bool ListJob::isPasswordProtected() const { return m_isPasswordProtected; } bool ListJob::isSingleFolderArchive() const { if (m_filesCount == 1 && m_dirCount == 0) { return false; } return m_isSingleFolderArchive; } void ListJob::onNewEntry(const ArchiveEntry& entry) { m_extractedFilesSize += entry[ Size ].toLongLong(); m_isPasswordProtected |= entry [ IsPasswordProtected ].toBool(); if (entry[IsDirectory].toBool()) { m_dirCount++; } else { m_filesCount++; } if (m_isSingleFolderArchive) { // RPM filenames have the ./ prefix, and "." would be detected as the subfolder name, so we remove it. const QString fileName = entry[FileName].toString().replace(QRegularExpression(QStringLiteral("^\\./")), QString()); const QString basePath = fileName.split(QLatin1Char('/')).at(0); if (m_basePath.isEmpty()) { m_basePath = basePath; m_subfolderName = basePath; } else { if (m_basePath != basePath) { m_isSingleFolderArchive = false; m_subfolderName.clear(); } } } } QString ListJob::subfolderName() const { if (!isSingleFolderArchive()) { return QString(); } return m_subfolderName; } ExtractJob::ExtractJob(const QVariantList& files, const QString& destinationDir, const ExtractionOptions& options, ReadOnlyArchiveInterface *interface, QObject *parent) : Job(interface, parent) , m_files(files) , m_destinationDir(destinationDir) , m_options(options) { qCDebug(ARK) << "ExtractJob created"; setDefaultOptions(); } void ExtractJob::doWork() { QString desc; if (m_files.count() == 0) { desc = i18n("Extracting all files"); } else { desc = i18np("Extracting one file", "Extracting %1 files", m_files.count()); } emit description(this, desc); QFileInfo destDirInfo(m_destinationDir); if (destDirInfo.isDir() && (!destDirInfo.isWritable() || !destDirInfo.isExecutable())) { onError(xi18n("Could not write to destination %1.Check whether you have sufficient permissions.", m_destinationDir), QString()); onFinished(false); return; } connectToArchiveInterfaceSignals(); qCDebug(ARK) << "Starting extraction with selected files:" << m_files << "Destination dir:" << m_destinationDir << "Options:" << m_options; bool ret = archiveInterface()->copyFiles(m_files, m_destinationDir, m_options); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void ExtractJob::setDefaultOptions() { ExtractionOptions defaultOptions; defaultOptions[QStringLiteral("PreservePaths")] = false; ExtractionOptions::const_iterator it = defaultOptions.constBegin(); for (; it != defaultOptions.constEnd(); ++it) { if (!m_options.contains(it.key())) { m_options[it.key()] = it.value(); } } } QString ExtractJob::destinationDirectory() const { return m_destinationDir; } ExtractionOptions ExtractJob::extractionOptions() const { return m_options; } AddJob::AddJob(const QStringList& files, const CompressionOptions& options , ReadWriteArchiveInterface *interface, QObject *parent) : Job(interface, parent) , m_files(files) , m_options(options) { qCDebug(ARK) << "AddJob started"; } void AddJob::doWork() { qCDebug(ARK) << "AddJob: going to add" << m_files.count() << "file(s)"; emit description(this, i18np("Adding a file", "Adding %1 files", m_files.count())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); const QString globalWorkDir = m_options.value(QStringLiteral("GlobalWorkDir")).toString(); const QDir workDir = globalWorkDir.isEmpty() ? QDir::current() : QDir(globalWorkDir); if (!globalWorkDir.isEmpty()) { qCDebug(ARK) << "GlobalWorkDir is set, changing dir to " << globalWorkDir; m_oldWorkingDir = QDir::currentPath(); QDir::setCurrent(globalWorkDir); } // The file paths must be relative to GlobalWorkDir. QStringList relativeFiles; foreach (const QString& file, m_files) { // #191821: workDir must be used instead of QDir::current() // so that symlinks aren't resolved automatically QString relativePath = workDir.relativeFilePath(file); if (file.endsWith(QLatin1Char('/'))) { relativePath += QLatin1Char('/'); } relativeFiles << relativePath; } connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->addFiles(relativeFiles, m_options); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void AddJob::onFinished(bool result) { if (!m_oldWorkingDir.isEmpty()) { QDir::setCurrent(m_oldWorkingDir); } Job::onFinished(result); } DeleteJob::DeleteJob(const QVariantList& files, ReadWriteArchiveInterface *interface, QObject *parent) : Job(interface, parent) , m_files(files) { } void DeleteJob::doWork() { emit description(this, i18np("Deleting a file from the archive", "Deleting %1 files", m_files.count())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->deleteFiles(m_files); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } +CommentJob::CommentJob(const QString& comment, ReadWriteArchiveInterface *interface, QObject *parent) + : Job(interface, parent) + , m_comment(comment) +{ +} + +void CommentJob::doWork() +{ + emit description(this, i18n("Adding comment")); + + ReadWriteArchiveInterface *m_writeInterface = + qobject_cast(archiveInterface()); + + Q_ASSERT(m_writeInterface); + + connectToArchiveInterfaceSignals(); + bool ret = m_writeInterface->addComment(m_comment); + + if (!archiveInterface()->waitForFinishedSignal()) { + onFinished(ret); + } +} + } // namespace Kerfuffle diff --git a/kerfuffle/jobs.h b/kerfuffle/jobs.h index d98a3120..5cbf83a8 100644 --- a/kerfuffle/jobs.h +++ b/kerfuffle/jobs.h @@ -1,178 +1,194 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef JOBS_H #define JOBS_H #include "kerfuffle_export.h" #include "archiveinterface.h" #include "archive_kerfuffle.h" #include "queries.h" #include #include #include #include #include namespace Kerfuffle { class KERFUFFLE_EXPORT Job : public KJob { Q_OBJECT public: void start(); bool isRunning() const; protected: Job(ReadOnlyArchiveInterface *interface, QObject *parent = 0); virtual ~Job(); virtual bool doKill(); virtual void emitResult(); ReadOnlyArchiveInterface *archiveInterface(); void connectToArchiveInterfaceSignals(); public slots: virtual void doWork() = 0; protected slots: virtual void onCancelled(); virtual void onError(const QString &message, const QString &details); virtual void onInfo(const QString &info); virtual void onEntry(const ArchiveEntry &archiveEntry); virtual void onProgress(double progress); virtual void onEntryRemoved(const QString &path); virtual void onFinished(bool result); virtual void onUserQuery(Query *query); signals: void entryRemoved(const QString & entry); void error(const QString& errorMessage, const QString& details); void newEntry(const ArchiveEntry &); void userQuery(Kerfuffle::Query*); private: ReadOnlyArchiveInterface *m_archiveInterface; bool m_isRunning; QElapsedTimer jobTimer; class Private; Private * const d; }; class KERFUFFLE_EXPORT ListJob : public Job { Q_OBJECT public: explicit ListJob(ReadOnlyArchiveInterface *interface, QObject *parent = 0); qlonglong extractedFilesSize() const; bool isPasswordProtected() const; bool isSingleFolderArchive() const; QString subfolderName() const; public slots: virtual void doWork() Q_DECL_OVERRIDE; private: bool m_isSingleFolderArchive; bool m_isPasswordProtected; QString m_subfolderName; QString m_basePath; qlonglong m_extractedFilesSize; qlonglong m_dirCount; qlonglong m_filesCount; private slots: void onNewEntry(const ArchiveEntry&); }; class KERFUFFLE_EXPORT ExtractJob : public Job { Q_OBJECT public: ExtractJob(const QVariantList& files, const QString& destinationDir, const ExtractionOptions& options, ReadOnlyArchiveInterface *interface, QObject *parent = 0); QString destinationDirectory() const; ExtractionOptions extractionOptions() const; public slots: virtual void doWork() Q_DECL_OVERRIDE; private: // TODO: Maybe this should be a method if ExtractionOptions were a class? void setDefaultOptions(); QVariantList m_files; QString m_destinationDir; ExtractionOptions m_options; }; class KERFUFFLE_EXPORT AddJob : public Job { Q_OBJECT public: AddJob(const QStringList& files, const CompressionOptions& options, ReadWriteArchiveInterface *interface, QObject *parent = 0); public slots: virtual void doWork() Q_DECL_OVERRIDE; protected slots: virtual void onFinished(bool result) Q_DECL_OVERRIDE; private: QString m_oldWorkingDir; QStringList m_files; CompressionOptions m_options; }; class KERFUFFLE_EXPORT DeleteJob : public Job { Q_OBJECT public: DeleteJob(const QVariantList& files, ReadWriteArchiveInterface *interface, QObject *parent = 0); public slots: virtual void doWork() Q_DECL_OVERRIDE; private: QVariantList m_files; }; +class KERFUFFLE_EXPORT CommentJob : public Job +{ + Q_OBJECT + +public: + CommentJob(const QString& comment, ReadWriteArchiveInterface *interface, QObject *parent = 0); + +public slots: + virtual void doWork() Q_DECL_OVERRIDE; + +private: + QString m_comment; +}; + + + } // namespace Kerfuffle #endif // JOBS_H diff --git a/part/ark_part.rc b/part/ark_part.rc index eb9c7112..1b2b823e 100644 --- a/part/ark_part.rc +++ b/part/ark_part.rc @@ -1,43 +1,44 @@ - + &Archive - + + &File &Settings Main Toolbar diff --git a/part/part.cpp b/part/part.cpp index 6e1063e3..cd6b4f7b 100644 --- a/part/part.cpp +++ b/part/part.cpp @@ -1,1271 +1,1340 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 Harald Hvaal * Copyright (C) 2009-2012 Raphael Kubo da Costa * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "part.h" #include "ark_debug.h" +#include "archiveformat.h" #include "archivemodel.h" #include "archiveview.h" #include "arkviewer.h" #include "dnddbusinterfaceadaptor.h" #include "infopanel.h" #include "jobtracker.h" #include "kerfuffle/archive_kerfuffle.h" #include "kerfuffle/extractiondialog.h" #include "kerfuffle/extractionsettingspage.h" #include "kerfuffle/jobs.h" #include "kerfuffle/settings.h" #include "kerfuffle/previewsettingspage.h" #include "kerfuffle/propertiesdialog.h" +#include "pluginmanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kerfuffle; K_PLUGIN_FACTORY(Factory, registerPlugin();) namespace Ark { static quint32 s_instanceCounter = 1; Part::Part(QWidget *parentWidget, QObject *parent, const QVariantList& args) : KParts::ReadWritePart(parent), m_splitter(Q_NULLPTR), m_busy(false), m_jobTracker(Q_NULLPTR) { Q_UNUSED(args) setComponentData(*createAboutData(), false); new DndExtractAdaptor(this); const QString pathName = QStringLiteral("/DndExtract/%1").arg(s_instanceCounter++); if (!QDBusConnection::sessionBus().registerObject(pathName, this)) { qCCritical(ARK) << "Could not register a D-Bus object for drag'n'drop"; } // m_vlayout is needed for later insertion of QMessageWidget QWidget *mainWidget = new QWidget; m_vlayout = new QVBoxLayout; m_model = new ArchiveModel(pathName, this); m_splitter = new QSplitter(Qt::Horizontal, parentWidget); m_view = new ArchiveView; m_infoPanel = new InfoPanel(m_model); // Add widgets for the comment field. m_commentView = new QPlainTextEdit(); m_commentView->setReadOnly(true); m_commentView->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); m_commentBox = new QGroupBox(i18n("Comment")); m_commentBox->hide(); QVBoxLayout *vbox = new QVBoxLayout; vbox->addWidget(m_commentView); m_commentBox->setLayout(vbox); + m_commentMsgWidget = new KMessageWidget(); + m_commentMsgWidget->setText(i18n("Comment has been modified.")); + m_commentMsgWidget->setMessageType(KMessageWidget::Information); + m_commentMsgWidget->setCloseButtonVisible(false); + m_commentMsgWidget->hide(); + + QAction *saveAction = new QAction(i18n("Save"), m_commentMsgWidget); + m_commentMsgWidget->addAction(saveAction); + connect(saveAction, &QAction::triggered, this, &Part::slotAddComment); + + m_commentBox->layout()->addWidget(m_commentMsgWidget); + + connect(m_commentView, &QPlainTextEdit::textChanged, this, &Part::slotCommentChanged); + setWidget(mainWidget); mainWidget->setLayout(m_vlayout); // Configure the QVBoxLayout and add widgets m_vlayout->setContentsMargins(0,0,0,0); m_vlayout->addWidget(m_splitter); // Vertical QSplitter for the file view and comment field. m_commentSplitter = new QSplitter(Qt::Vertical, parentWidget); m_commentSplitter->setOpaqueResize(false); m_commentSplitter->addWidget(m_view); m_commentSplitter->addWidget(m_commentBox); m_commentSplitter->setCollapsible(0, false); // Horizontal QSplitter for the file view and infopanel. m_splitter->addWidget(m_commentSplitter); m_splitter->addWidget(m_infoPanel); // Read settings from config file and show/hide infoPanel. if (!ArkSettings::showInfoPanel()) { m_infoPanel->hide(); } else { m_splitter->setSizes(ArkSettings::splitterSizes()); } setupView(); setupActions(); connect(m_model, &ArchiveModel::loadingStarted, this, &Part::slotLoadingStarted); connect(m_model, &ArchiveModel::loadingFinished, this, &Part::slotLoadingFinished); connect(m_model, SIGNAL(droppedFiles(QStringList,QString)), this, SLOT(slotAddFiles(QStringList,QString))); connect(m_model, &ArchiveModel::error, this, &Part::slotError); connect(this, &Part::busy, this, &Part::setBusyGui); connect(this, &Part::ready, this, &Part::setReadyGui); connect(this, SIGNAL(completed()), this, SLOT(setFileNameFromArchive())); m_statusBarExtension = new KParts::StatusBarExtension(this); setXMLFile(QStringLiteral("ark_part.rc")); } Part::~Part() { qDeleteAll(m_tmpOpenDirList); // Only save splitterSizes if infopanel is visible, // because we don't want to store zero size for infopanel. if (m_showInfoPanelAction->isChecked()) { ArkSettings::setSplitterSizes(m_splitter->sizes()); } ArkSettings::setShowInfoPanel(m_showInfoPanelAction->isChecked()); ArkSettings::self()->save(); m_extractArchiveAction->menu()->deleteLater(); m_extractAction->menu()->deleteLater(); } +void Part::slotCommentChanged() +{ + if (m_commentMsgWidget->isHidden() && m_commentView->toPlainText() != m_model->archive()->comment()) { + m_commentMsgWidget->animatedShow(); + } else if (m_commentMsgWidget->isVisible() && m_commentView->toPlainText() == m_model->archive()->comment()) { + m_commentMsgWidget->hide(); + } +} + KAboutData *Part::createAboutData() { return new KAboutData(QStringLiteral("ark"), i18n("ArkPart"), QStringLiteral("3.0")); } void Part::registerJob(KJob* job) { if (!m_jobTracker) { m_jobTracker = new JobTracker(widget()); m_statusBarExtension->addStatusBarItem(m_jobTracker->widget(0), 0, true); m_jobTracker->widget(job)->show(); } m_jobTracker->registerJob(job); emit busy(); connect(job, &KJob::result, this, &Part::ready); } // TODO: KIO::mostLocalHere is used here to resolve some KIO URLs to local // paths (e.g. desktop:/), but more work is needed to support extraction // to non-local destinations. See bugs #189322 and #204323. void Part::extractSelectedFilesTo(const QString& localPath) { if (!m_model) { return; } const QUrl url = QUrl::fromUserInput(localPath, QString()); KIO::StatJob* statJob = nullptr; // Try to resolve the URL to a local path. if (!url.isLocalFile() && !url.scheme().isEmpty()) { statJob = KIO::mostLocalUrl(url); if (!statJob->exec() || statJob->error() != 0) { return; } } const QString destination = statJob ? statJob->statResult().stringValue(KIO::UDSEntry::UDS_LOCAL_PATH) : localPath; delete statJob; // The URL could not be resolved to a local path. if (!url.isLocalFile() && destination.isEmpty()) { qCWarning(ARK) << "Ark cannot extract to non-local destination:" << localPath; KMessageBox::sorry(widget(), xi18nc("@info", "Ark can only extract to local destinations.")); return; } qCDebug(ARK) << "Extract to" << destination; Kerfuffle::ExtractionOptions options; options[QStringLiteral("PreservePaths")] = true; options[QStringLiteral("RemoveRootNode")] = true; options[QStringLiteral("DragAndDrop")] = true; // Create and start the ExtractJob. ExtractJob *job = m_model->extractFiles(filesAndRootNodesForIndexes(addChildren(m_view->selectionModel()->selectedRows())), destination, options); registerJob(job); connect(job, &KJob::result, this, &Part::slotExtractionDone); job->start(); } void Part::setupView() { m_view->setContextMenuPolicy(Qt::CustomContextMenu); m_view->setModel(m_model); m_view->setSortingEnabled(true); connect(m_view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Part::updateActions); connect(m_view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Part::selectionChanged); connect(m_view, &QTreeView::activated, this, &Part::slotActivated); connect(m_view, &QWidget::customContextMenuRequested, this, &Part::slotShowContextMenu); connect(m_model, &QAbstractItemModel::columnsInserted, this, &Part::adjustColumns); } void Part::slotActivated(QModelIndex) { // The activated signal is emitted when items are selected with the mouse, // so do nothing if CTRL or SHIFT key is pressed. if (QGuiApplication::keyboardModifiers() != Qt::ShiftModifier && QGuiApplication::keyboardModifiers() != Qt::ControlModifier) { ArkSettings::defaultOpenAction() == ArkSettings::EnumDefaultOpenAction::Preview ? slotOpenEntry(Preview) : slotOpenEntry(OpenFile); } } void Part::setupActions() { // We use a QSignalMapper for the preview, open and openwith actions. This // way we can connect all three actions to the same slot slotOpenEntry and // pass the OpenFileMode as argument to the slot. m_signalMapper = new QSignalMapper; m_showInfoPanelAction = new KToggleAction(i18nc("@action:inmenu", "Show information panel"), this); actionCollection()->addAction(QStringLiteral( "show-infopanel" ), m_showInfoPanelAction); m_showInfoPanelAction->setChecked(ArkSettings::showInfoPanel()); connect(m_showInfoPanelAction, &QAction::triggered, this, &Part::slotToggleInfoPanel); m_saveAsAction = actionCollection()->addAction(KStandardAction::SaveAs, QStringLiteral("ark_file_save_as"), this, SLOT(slotSaveAs())); m_openFileAction = actionCollection()->addAction(QStringLiteral("openfile")); m_openFileAction->setText(i18nc("open a file with external program", "&Open")); m_openFileAction->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); m_openFileAction->setToolTip(i18nc("@info:tooltip", "Click to open the selected file with the associated application")); connect(m_openFileAction, SIGNAL(triggered(bool)), m_signalMapper, SLOT(map())); m_signalMapper->setMapping(m_openFileAction, OpenFile); m_openFileWithAction = actionCollection()->addAction(QStringLiteral("openfilewith")); m_openFileWithAction->setText(i18nc("open a file with external program", "Open &With...")); m_openFileWithAction->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); m_openFileWithAction->setToolTip(i18nc("@info:tooltip", "Click to open the selected file with an external program")); connect(m_openFileWithAction, SIGNAL(triggered(bool)), m_signalMapper, SLOT(map())); m_signalMapper->setMapping(m_openFileWithAction, OpenFileWith); m_previewAction = actionCollection()->addAction(QStringLiteral("preview")); m_previewAction->setText(i18nc("to preview a file inside an archive", "Pre&view")); m_previewAction->setIcon(QIcon::fromTheme(QStringLiteral("document-preview-archive"))); m_previewAction->setToolTip(i18nc("@info:tooltip", "Click to preview the selected file")); actionCollection()->setDefaultShortcut(m_previewAction, Qt::CTRL + Qt::Key_P); connect(m_previewAction, SIGNAL(triggered(bool)), m_signalMapper, SLOT(map())); m_signalMapper->setMapping(m_previewAction, Preview); m_extractArchiveAction = actionCollection()->addAction(QStringLiteral("extract_all")); m_extractArchiveAction->setText(i18nc("@action:inmenu", "E&xtract All")); m_extractArchiveAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract"))); m_extractArchiveAction->setToolTip(i18n("Click to open an extraction dialog, where you can choose how to extract all the files in the archive")); actionCollection()->setDefaultShortcut(m_extractArchiveAction, Qt::CTRL + Qt::SHIFT + Qt::Key_E); connect(m_extractArchiveAction, &QAction::triggered, this, &Part::slotExtractArchive); m_extractAction = actionCollection()->addAction(QStringLiteral("extract")); m_extractAction->setText(i18nc("@action:inmenu", "&Extract")); m_extractAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract"))); actionCollection()->setDefaultShortcut(m_extractAction, Qt::CTRL + Qt::Key_E); m_extractAction->setToolTip(i18n("Click to open an extraction dialog, where you can choose to extract either all files or just the selected ones")); connect(m_extractAction, &QAction::triggered, this, &Part::slotShowExtractionDialog); m_addFilesAction = actionCollection()->addAction(QStringLiteral("add")); m_addFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-insert"))); m_addFilesAction->setText(i18n("Add &File...")); m_addFilesAction->setToolTip(i18nc("@info:tooltip", "Click to add files to the archive")); connect(m_addFilesAction, SIGNAL(triggered(bool)), this, SLOT(slotAddFiles())); m_addDirAction = actionCollection()->addAction(QStringLiteral("add-dir")); m_addDirAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-insert-directory"))); m_addDirAction->setText(i18n("Add Fo&lder...")); m_addDirAction->setToolTip(i18nc("@info:tooltip", "Click to add a folder to the archive")); connect(m_addDirAction, &QAction::triggered, this, &Part::slotAddDir); m_deleteFilesAction = actionCollection()->addAction(QStringLiteral("delete")); m_deleteFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-remove"))); m_deleteFilesAction->setText(i18n("De&lete")); actionCollection()->setDefaultShortcut(m_deleteFilesAction, Qt::Key_Delete); m_deleteFilesAction->setToolTip(i18nc("@info:tooltip", "Click to delete the selected files")); connect(m_deleteFilesAction, &QAction::triggered, this, &Part::slotDeleteFiles); m_propertiesAction = actionCollection()->addAction(QStringLiteral("properties")); m_propertiesAction->setIcon(QIcon::fromTheme(QStringLiteral("document-properties"))); m_propertiesAction->setText(i18nc("@action:inmenu", "&Properties")); actionCollection()->setDefaultShortcut(m_propertiesAction, Qt::ALT + Qt::Key_Return); m_propertiesAction->setToolTip(i18nc("@info:tooltip", "Click to see properties for archive")); connect(m_propertiesAction, &QAction::triggered, this, &Part::slotShowProperties); + m_editCommentAction = actionCollection()->addAction(QStringLiteral("edit_comment")); + m_editCommentAction->setIcon(QIcon::fromTheme(QStringLiteral("document-edit"))); + m_editCommentAction->setText(i18nc("@action:inmenu", "&Edit Comment")); + 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); + connect(m_signalMapper, SIGNAL(mapped(int)), this, SLOT(slotOpenEntry(int))); updateActions(); updateQuickExtractMenu(m_extractArchiveAction); updateQuickExtractMenu(m_extractAction); } void Part::updateActions() { bool isWritable = m_model->archive() && !m_model->archive()->isReadOnly(); bool isDirectory = m_model->entryForIndex(m_view->selectionModel()->currentIndex())[IsDirectory].toBool(); 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(); const qlonglong size = m_model->entryForIndex(m_view->selectionModel()->currentIndex())[Size].toLongLong(); bool isPreviewable = (!limit || (limit && size < maxPreviewSize)); m_previewAction->setEnabled(!isBusy() && isPreviewable && !isDirectory && (selectedEntriesCount == 1)); m_extractArchiveAction->setEnabled(!isBusy() && (m_model->rowCount() > 0)); m_extractAction->setEnabled(!isBusy() && (m_model->rowCount() > 0)); m_saveAsAction->setEnabled(!isBusy() && m_model->rowCount() > 0); m_addFilesAction->setEnabled(!isBusy() && isWritable); m_addDirAction->setEnabled(!isBusy() && isWritable); m_deleteFilesAction->setEnabled(!isBusy() && isWritable && (selectedEntriesCount > 0)); m_openFileAction->setEnabled(!isBusy() && isPreviewable && !isDirectory && (selectedEntriesCount == 1)); m_openFileWithAction->setEnabled(!isBusy() && isPreviewable && !isDirectory && (selectedEntriesCount == 1)); m_propertiesAction->setEnabled(!isBusy() && m_model->archive()); + + m_commentView->setEnabled(!isBusy()); + m_commentMsgWidget->setEnabled(!isBusy()); + + 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_model->archive()->comment().isEmpty() ? m_editCommentAction->setText(i18nc("@action:inmenu", "Add &Comment")) : m_editCommentAction->setText(i18nc("@action:inmenu", "Edit &Comment")); + } else { + m_editCommentAction->setEnabled(false); + m_commentView->setReadOnly(true); + } +} + +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::updateQuickExtractMenu(QAction *extractAction) { if (!extractAction) { return; } QMenu *menu = extractAction->menu(); if (!menu) { menu = new QMenu(); extractAction->setMenu(menu); connect(menu, &QMenu::triggered, this, &Part::slotQuickExtractFiles); // Remember to keep this action's properties as similar to // extractAction's as possible (except where it does not make // sense, such as the text or the shortcut). QAction *extractTo = menu->addAction(i18n("Extract To...")); extractTo->setIcon(extractAction->icon()); extractTo->setToolTip(extractAction->toolTip()); if (extractAction == m_extractArchiveAction) { connect(extractTo, &QAction::triggered, this, &Part::slotExtractArchive); } else { connect(extractTo, &QAction::triggered, this, &Part::slotShowExtractionDialog); } menu->addSeparator(); QAction *header = menu->addAction(i18n("Quick Extract To...")); header->setEnabled(false); header->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract"))); } while (menu->actions().size() > 3) { menu->removeAction(menu->actions().last()); } const KConfigGroup conf(KSharedConfig::openConfig(), "ExtractDialog"); const QStringList dirHistory = conf.readPathEntry("DirHistory", QStringList()); for (int i = 0; i < qMin(10, dirHistory.size()); ++i) { const QString dir = QUrl(dirHistory.value(i)).toString(QUrl::RemoveScheme | QUrl::NormalizePathSegments | QUrl::PreferLocalFile); if (QDir(dir).exists()) { QAction *newAction = menu->addAction(dir); newAction->setData(dir); } } } void Part::slotQuickExtractFiles(QAction *triggeredAction) { // #190507: triggeredAction->data.isNull() means it's the "Extract to..." // action, and we do not want it to run here if (!triggeredAction->data().isNull()) { const QString userDestination = triggeredAction->data().toString(); qCDebug(ARK) << "Extract to user dest" << userDestination; QString finalDestinationDirectory; const QString detectedSubfolder = detectSubfolder(); qCDebug(ARK) << "Detected subfolder" << detectedSubfolder; if (!isSingleFolderArchive()) { finalDestinationDirectory = userDestination + QDir::separator() + detectedSubfolder; QDir(userDestination).mkdir(detectedSubfolder); } else { finalDestinationDirectory = userDestination; } qCDebug(ARK) << "Extract to final dest" << finalDestinationDirectory; Kerfuffle::ExtractionOptions options; options[QStringLiteral("PreservePaths")] = true; QList files = filesAndRootNodesForIndexes(m_view->selectionModel()->selectedRows()); ExtractJob *job = m_model->extractFiles(files, finalDestinationDirectory, options); registerJob(job); connect(job, &KJob::result, this, &Part::slotExtractionDone); job->start(); } } void Part::selectionChanged() { m_infoPanel->setIndexes(m_view->selectionModel()->selectedRows()); } bool Part::openFile() { qCDebug(ARK) << "Attempting to open archive" << localFilePath(); if (!isLocalFileValid()) { return false; } QScopedPointer archive(Kerfuffle::Archive::create(localFilePath(), m_model)); Q_ASSERT(archive); if (archive->error() == NoPlugin) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Ark was not able to open %1. No suitable plugin found." "Ark does not seem to support this file type.", QFileInfo(localFilePath()).fileName())); return false; } if (archive->error() == FailedPlugin) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Ark was not able to open %1. Failed to load a suitable plugin." "Make sure any executables needed to handle the archive type are installed.", QFileInfo(localFilePath()).fileName())); return false; } Q_ASSERT(archive->isValid()); // Plugin loaded successfully. KJob *job = m_model->setArchive(archive.take()); if (job) { registerJob(job); job->start(); } else { updateActions(); } m_infoPanel->setIndex(QModelIndex()); if (arguments().metaData()[QStringLiteral("showExtractDialog")] == QLatin1String("true")) { QTimer::singleShot(0, this, &Part::slotShowExtractionDialog); } const QString password = arguments().metaData()[QStringLiteral("encryptionPassword")]; if (!password.isEmpty()) { m_model->encryptArchive(password, arguments().metaData()[QStringLiteral("encryptHeader")] == QLatin1String("true")); } return true; } bool Part::saveFile() { return true; } bool Part::isBusy() const { return m_busy; } KConfigSkeleton *Part::config() const { return ArkSettings::self(); } QList Part::settingsPages(QWidget *parent) const { QList pages; pages.append(new ExtractionSettingsPage(parent, i18nc("@title:tab", "Extraction Settings"), QStringLiteral("archive-extract"))); pages.append(new PreviewSettingsPage(parent, i18nc("@title:tab", "Preview Settings"), QStringLiteral("document-preview-archive"))); return pages; } bool Part::isLocalFileValid() { const QString localFile = localFilePath(); const QFileInfo localFileInfo(localFile); const bool creatingNewArchive = arguments().metaData()[QStringLiteral("createNewArchive")] == QLatin1String("true"); if (localFileInfo.isDir()) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "%1 is a directory.", localFile)); return false; } if (creatingNewArchive) { if (localFileInfo.exists()) { if (!confirmAndDelete(localFile)) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Could not overwrite %1. Check whether you have write permission.", localFile)); return false; } } displayMsgWidget(KMessageWidget::Information, xi18nc("@info", "The archive %1 will be created as soon as you add a file.", localFile)); } else { if (!localFileInfo.exists()) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "The archive %1 was not found.", localFile)); return false; } if (!localFileInfo.isReadable()) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "The archive %1 could not be loaded, as it was not possible to read from it.", localFile)); return false; } } return true; } bool Part::confirmAndDelete(const QString &targetFile) { QFileInfo targetInfo(targetFile); const auto buttonCode = KMessageBox::warningYesNo(widget(), xi18nc("@info", "The archive %1 already exists. Do you wish to overwrite it?", targetInfo.fileName()), i18nc("@title:window", "File Exists"), KGuiItem(i18nc("@action:button", "Overwrite")), KStandardGuiItem::cancel()); if (buttonCode != KMessageBox::Yes || !targetInfo.isWritable()) { return false; } qCDebug(ARK) << "Removing file" << targetFile; return QFile(targetFile).remove(); } void Part::slotLoadingStarted() { } void Part::slotLoadingFinished(KJob *job) { if (job->error()) { if (arguments().metaData()[QStringLiteral("createNewArchive")] != QLatin1String("true")) { if (job->error() != KJob::KilledJobError) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Loading the archive %1 failed with the following error:%2", localFilePath(), job->errorText())); } // The file failed to open, so reset the open archive, info panel and caption. m_model->setArchive(Q_NULLPTR); m_infoPanel->setPrettyFileName(QString()); m_infoPanel->updateWithDefaults(); emit setWindowCaption(QString()); } } m_view->sortByColumn(0, Qt::AscendingOrder); // #303708: expand the first level only when there is just one root folder. // Typical use case: an archive with source files. if (m_view->model()->rowCount() == 1) { m_view->expandToDepth(0); } // After loading all files, resize the columns to fit all fields m_view->header()->resizeSections(QHeaderView::ResizeToContents); updateActions(); if (!m_model->archive()) { return; } if (!m_model->archive()->comment().isEmpty()) { m_commentView->setPlainText(m_model->archive()->comment()); - m_commentBox->show(); - m_commentSplitter->setSizes(QList() << m_view->height() * 0.6 << 1); + 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))[FileName].toString() == QLatin1String("README.TXT")) { qCWarning(ARK) << "Detected ISO image with UDF filesystem"; displayMsgWidget(KMessageWidget::Warning, xi18nc("@info", "Ark does not currently support ISO files with UDF filesystem.")); } } } void Part::setReadyGui() { QApplication::restoreOverrideCursor(); m_busy = false; if (m_statusBarExtension->statusBar()) { m_statusBarExtension->statusBar()->hide(); } m_view->setEnabled(true); updateActions(); } void Part::setBusyGui() { QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); m_busy = true; if (m_statusBarExtension->statusBar()) { m_statusBarExtension->statusBar()->show(); } m_view->setEnabled(false); updateActions(); } void Part::setFileNameFromArchive() { const QString prettyName = url().fileName(); m_infoPanel->setPrettyFileName(prettyName); m_infoPanel->updateWithDefaults(); emit setWindowCaption(prettyName); } void Part::slotOpenEntry(int mode) { qCDebug(ARK) << "Opening with mode" << mode; QModelIndex index = m_view->selectionModel()->currentIndex(); const ArchiveEntry& entry = m_model->entryForIndex(index); // Don't open directories. if (entry[IsDirectory].toBool()) { return; } // We don't support opening symlinks. if (entry[Link].toBool()) { displayMsgWidget(KMessageWidget::Information, i18n("Ark cannot open symlinks.")); return; } // Extract the entry. if (!entry.isEmpty()) { Kerfuffle::ExtractionOptions options; options[QStringLiteral("PreservePaths")] = true; m_tmpOpenDirList.append(new QTemporaryDir); m_openFileMode = static_cast(mode); ExtractJob *job = m_model->extractFile(entry[InternalID], m_tmpOpenDirList.last()->path(), options); registerJob(job); connect(job, &KJob::result, this, &Part::slotOpenExtractedEntry); job->start(); } } void Part::slotOpenExtractedEntry(KJob *job) { // FIXME: the error checking here isn't really working // if there's an error or an overwrite dialog, // the preview dialog will be launched anyway if (!job->error()) { const ArchiveEntry& entry = m_model->entryForIndex(m_view->selectionModel()->currentIndex()); ExtractJob *extractJob = qobject_cast(job); Q_ASSERT(extractJob); QString fullName = extractJob->destinationDirectory() + QLatin1Char('/') + entry[FileName].toString(); // Make sure a maliciously crafted archive with parent folders named ".." do // not cause the previewed file path to be located outside the temporary // directory, resulting in a directory traversal issue. fullName.remove(QStringLiteral("../")); 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); } // TODO: get rid of m_openFileMode by extending ExtractJob with a // Preview/OpenJob. This would prevent race conditions if we ever stop // disabling the whole UI while extracting a file to preview it. if (m_openFileMode != Preview && isWritable) { m_fileWatcher = new QFileSystemWatcher; connect(m_fileWatcher, &QFileSystemWatcher::fileChanged, this, &Part::slotWatchedFileModified); } QMimeDatabase db; switch (m_openFileMode) { case Preview: ArkViewer::view(fullName); break; case OpenFile: KRun::runUrl(QUrl::fromUserInput(fullName, QString(), QUrl::AssumeLocalFile), db.mimeTypeForFile(fullName).name(), widget()); break; case OpenFileWith: QList list; list.append(QUrl::fromUserInput(fullName, QString(), QUrl::AssumeLocalFile)); KRun::displayOpenWithDialog(list, widget(), true); break; } if (m_openFileMode != Preview && isWritable) { m_fileWatcher->addPath(fullName); } } else if (job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } setReadyGui(); } void Part::slotWatchedFileModified(const QString& file) { qCDebug(ARK) << "Watched file modified:" << file; // Find the relative path of the file within the archive. QString relPath = file; foreach (QTemporaryDir *tmpDir, m_tmpOpenDirList) { relPath.remove(tmpDir->path()); //Remove tmpDir. } relPath = relPath.mid(1); //Remove leading slash. if (relPath.contains(QLatin1Char('/'))) { relPath = relPath.section(QLatin1Char('/'), 0, -2); //Remove filename. } else { // File is in the root of the archive, no path. relPath = QString(); } // Set up a string for display in KMessageBox. QString prettyFilename; if (relPath.isEmpty()) { prettyFilename = file.section(QLatin1Char('/'), -1); } else { prettyFilename = relPath + QLatin1Char('/') + file.section(QLatin1Char('/'), -1); } if (KMessageBox::questionYesNo(widget(), xi18n("The file %1 was modified. Do you want to update the archive?", prettyFilename), i18nc("@title:window", "File Modified")) == KMessageBox::Yes) { QStringList list = QStringList() << file; qCDebug(ARK) << "Updating file" << file << "with path" << relPath; slotAddFiles(list, relPath); } // This is needed because some apps, such as Kate, delete and recreate // files when saving. m_fileWatcher->addPath(file); } void Part::slotError(const QString& errorMessage, const QString& details) { if (details.isEmpty()) { KMessageBox::error(widget(), errorMessage); } else { KMessageBox::detailedError(widget(), errorMessage, details); } } bool Part::isSingleFolderArchive() const { return m_model->archive()->isSingleFolderArchive(); } QString Part::detectSubfolder() const { if (!m_model) { return QString(); } return m_model->archive()->subfolderName(); } void Part::slotExtractArchive() { if (m_view->selectionModel()->selectedRows().count() > 0) { m_view->selectionModel()->clear(); } slotShowExtractionDialog(); } void Part::slotShowExtractionDialog() { if (!m_model) { return; } QPointer dialog(new Kerfuffle::ExtractionDialog); dialog.data()->setModal(true); if (m_view->selectionModel()->selectedRows().count() > 0) { dialog.data()->setShowSelectedFiles(true); } dialog.data()->setSingleFolderArchive(isSingleFolderArchive()); dialog.data()->setSubfolder(detectSubfolder()); dialog.data()->setCurrentUrl(QUrl::fromLocalFile(QFileInfo(m_model->archive()->fileName()).absolutePath())); dialog.data()->show(); dialog.data()->restoreWindowSize(); if (dialog.data()->exec()) { updateQuickExtractMenu(m_extractArchiveAction); updateQuickExtractMenu(m_extractAction); QVariantList 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 { QVariantList ret; foreach(const QModelIndex& index, list) { const ArchiveEntry& entry = m_model->entryForIndex(index); ret << entry[InternalID].toString(); } return ret; } QList Part::filesAndRootNodesForIndexes(const QModelIndexList& list) const { QVariantList fileList; foreach (const QModelIndex& index, list) { // Find the topmost unselected parent. This is done by iterating up // through the directory hierarchy and see if each parent is included // in the selection OR if the parent is already part of list. // The latter is needed for unselected folders which are subfolders of // a selected parent folder. QModelIndex selectionRoot = index.parent(); while (m_view->selectionModel()->isSelected(selectionRoot) || list.contains(selectionRoot)) { selectionRoot = selectionRoot.parent(); } // Fetch the root node for the unselected parent. const QString rootInternalID = m_model->entryForIndex(selectionRoot).value(InternalID).toString(); // Append index with root node to fileList. QModelIndexList alist = QModelIndexList() << index; foreach (const QVariant &file, filesForIndexes(alist)) { QVariant v = QVariant::fromValue(fileRootNodePair(file.toString(), rootInternalID)); if (!fileList.contains(v)) { fileList.append(v); } } } return fileList; } void Part::slotExtractionDone(KJob* job) { if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } else { ExtractJob *extractJob = qobject_cast(job); Q_ASSERT(extractJob); const bool followExtractionDialogSettings = extractJob->extractionOptions().value(QStringLiteral("FollowExtractionDialogSettings"), false).toBool(); if (!followExtractionDialogSettings) { return; } if (ArkSettings::openDestinationFolderAfterExtraction()) { qCDebug(ARK) << "Shall open" << extractJob->destinationDirectory(); QUrl destinationDirectory = QUrl::fromLocalFile(extractJob->destinationDirectory()).adjusted(QUrl::NormalizePathSegments); qCDebug(ARK) << "Shall open URL" << destinationDirectory; KRun::runUrl(destinationDirectory, QStringLiteral("inode/directory"), widget()); } if (ArkSettings::closeAfterExtraction()) { emit quit(); } } } void Part::adjustColumns() { m_view->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); } void Part::slotAddFiles(const QStringList& filesToAdd, const QString& path) { if (filesToAdd.isEmpty()) { return; } qCDebug(ARK) << "Adding " << filesToAdd << " to " << path; // Add a trailing slash to directories. QStringList cleanFilesToAdd(filesToAdd); for (int i = 0; i < cleanFilesToAdd.size(); ++i) { QString& file = cleanFilesToAdd[i]; if (QFileInfo(file).isDir()) { if (!file.endsWith(QLatin1Char( '/' ))) { file += QLatin1Char( '/' ); } } } // GlobalWorkDir is used by AddJob and should contain the part of the // absolute path of files to be added that should NOT be included in the // directory structure within the archive. // Example: We add file "/home/user/somedir/somefile.txt" and want the file // to have the relative path within the archive "somedir/somefile.txt". // GlobalWorkDir is then: "/home/user" QString globalWorkDir = cleanFilesToAdd.first(); // path represents the path of the file within the archive. This needs to // be removed from globalWorkDir, otherwise the files will be added to the // root of the archive. In the example above, path would be "somedir/". if (!path.isEmpty()) { globalWorkDir.remove(path); } // Remove trailing slash (needed when adding dirs). if (globalWorkDir.right(1) == QLatin1String("/")) { globalWorkDir.chop(1); } // Now take the absolute path of the parent directory. globalWorkDir = QFileInfo(globalWorkDir).dir().absolutePath(); qCDebug(ARK) << "Detected GlobalWorkDir to be " << globalWorkDir; CompressionOptions options; options[QStringLiteral("GlobalWorkDir")] = globalWorkDir; if (arguments().metaData().contains(QStringLiteral("compressionLevel"))) { options[QStringLiteral("CompressionLevel")] = arguments().metaData()[QStringLiteral("compressionLevel")]; } AddJob *job = m_model->addFiles(cleanFilesToAdd, options); if (!job) { return; } connect(job, &KJob::result, this, &Part::slotAddFilesDone); registerJob(job); job->start(); } void Part::slotAddFiles() { // #264819: passing widget() as the parent will not work as expected. // KFileDialog will create a KFileWidget, which runs an internal // event loop to stat the given directory. This, in turn, leads to // events being delivered to widget(), which is a QSplitter, which // in turn reimplements childEvent() and will end up calling // QWidget::show() on the KFileDialog (thus showing it in a // non-modal state). // When KFileDialog::exec() is called, the widget is already shown // and nothing happens. const QStringList filesToAdd = QFileDialog::getOpenFileNames(widget(), i18nc("@title:window", "Add Files")); slotAddFiles(filesToAdd); } void Part::slotAddDir() { const QString dirToAdd = QFileDialog::getExistingDirectory(widget(), i18nc("@title:window", "Add Folder")); if (!dirToAdd.isEmpty()) { slotAddFiles(QStringList() << dirToAdd); } } void Part::slotAddFilesDone(KJob* job) { if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } } void Part::slotDeleteFilesDone(KJob* job) { if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } } void Part::slotDeleteFiles() { const int selectionsCount = m_view->selectionModel()->selectedRows().count(); const auto reallyDelete = KMessageBox::questionYesNo(widget(), i18ncp("@info", "Deleting this file is not undoable. Are you sure you want to do this?", "Deleting these files is not undoable. Are you sure you want to do this?", selectionsCount), i18ncp("@title:window", "Delete File", "Delete Files", selectionsCount), KStandardGuiItem::del(), KStandardGuiItem::no(), QString(), KMessageBox::Dangerous | KMessageBox::Notify); if (reallyDelete == KMessageBox::No) { return; } DeleteJob *job = m_model->deleteFiles(filesForIndexes(addChildren(m_view->selectionModel()->selectedRows()))); connect(job, &KJob::result, this, &Part::slotDeleteFilesDone); registerJob(job); job->start(); } void Part::slotShowProperties() { QPointer dialog(new Kerfuffle::PropertiesDialog(0, m_model->archive())); dialog.data()->show(); } void Part::slotToggleInfoPanel(bool visible) { if (visible) { m_splitter->setSizes(ArkSettings::splitterSizes()); m_infoPanel->show(); } else { // We need to save the splitterSizes before hiding, otherwise // Ark won't remember resizing done by the user. ArkSettings::setSplitterSizes(m_splitter->sizes()); m_infoPanel->hide(); } } void Part::slotSaveAs() { QUrl saveUrl = QFileDialog::getSaveFileUrl(widget(), i18nc("@title:window", "Save Archive As"), url()); if ((saveUrl.isValid()) && (!saveUrl.isEmpty())) { auto statJob = KIO::stat(saveUrl, KIO::StatJob::DestinationSide, 0); KJobWidgets::setWindow(statJob, widget()); if (statJob->exec()) { int overwrite = KMessageBox::warningContinueCancel(widget(), xi18nc("@info", "An archive named %1 already exists. Are you sure you want to overwrite it?", saveUrl.fileName()), QString(), KStandardGuiItem::overwrite()); if (overwrite != KMessageBox::Continue) { return; } } QUrl srcUrl = QUrl::fromLocalFile(localFilePath()); if (!QFile::exists(localFilePath())) { if (url().isLocalFile()) { KMessageBox::error(widget(), xi18nc("@info", "The archive %1 cannot be copied to the specified location. The archive does not exist anymore.", localFilePath())); return; } else { srcUrl = url(); } } KIO::Job *copyJob = KIO::file_copy(srcUrl, saveUrl, -1, KIO::Overwrite); KJobWidgets::setWindow(copyJob, widget()); copyJob->exec(); if (copyJob->error()) { KMessageBox::error(widget(), xi18nc("@info", "The archive could not be saved as %1. Try saving it to another location.", saveUrl.path())); } } } void Part::slotShowContextMenu() { if (!factory()) { return; } QMenu *popup = static_cast(factory()->container(QStringLiteral("context_menu"), this)); popup->popup(QCursor::pos()); } void Part::displayMsgWidget(KMessageWidget::MessageType type, const QString& msg) { KMessageWidget *msgWidget = new KMessageWidget(); msgWidget->setText(msg); msgWidget->setMessageType(type); m_vlayout->insertWidget(0, msgWidget); msgWidget->animatedShow(); } } // namespace Ark #include "part.moc" diff --git a/part/part.h b/part/part.h index a8267a2d..8f42bff5 100644 --- a/part/part.h +++ b/part/part.h @@ -1,172 +1,177 @@ /* * 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 #include #include #include #include class ArchiveModel; 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 slotOpenEntry(int mode); void slotError(const QString& errorMessage, const QString& details); void slotExtractArchive(); void slotShowExtractionDialog(); void slotExtractionDone(KJob*); void slotQuickExtractFiles(QAction*); void slotAddFiles(); void slotAddFiles(const QStringList& files, const QString& path = QString()); void slotAddDir(); void slotAddFilesDone(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(); signals: void busy(); void ready(); void quit(); private: 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; QAction *m_previewAction; QAction *m_openFileAction; QAction *m_openFileWithAction; QAction *m_extractArchiveAction; QAction *m_extractAction; QAction *m_addFilesAction; QAction *m_addDirAction; QAction *m_deleteFilesAction; QAction *m_saveAsAction; QAction *m_propertiesAction; + QAction *m_editCommentAction; KToggleAction *m_showInfoPanelAction; InfoPanel *m_infoPanel; QSplitter *m_splitter; QList m_tmpOpenDirList; bool m_busy; OpenFileMode m_openFileMode; 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; }; } // namespace Ark #endif // PART_H diff --git a/plugins/clirarplugin/cliplugin.cpp b/plugins/clirarplugin/cliplugin.cpp index e1390bb5..7e753533 100644 --- a/plugins/clirarplugin/cliplugin.cpp +++ b/plugins/clirarplugin/cliplugin.cpp @@ -1,511 +1,515 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2010-2011,2014 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 "cliplugin.h" #include "ark_debug.h" #include "kerfuffle/kerfuffle_export.h" #include #include #include using namespace Kerfuffle; K_PLUGIN_FACTORY_WITH_JSON(CliPluginFactory, "kerfuffle_clirar.json", registerPlugin();) CliPlugin::CliPlugin(QObject *parent, const QVariantList& args) : CliInterface(parent, args) , m_parseState(ParseStateTitle) , m_isUnrar5(false) , m_isPasswordProtected(false) , m_isMultiVolume(false) , m_isSolid(false) , m_remainingIgnoreLines(1) //The first line of UNRAR output is empty. , m_linesComment(0) { qCDebug(ARK) << "Loaded cli_rar plugin"; // Empty lines are needed for parsing output of unrar. setListEmptyLines(true); } CliPlugin::~CliPlugin() { } void CliPlugin::resetParsing() { m_parseState = ParseStateTitle; m_remainingIgnoreLines = 1; m_comment.clear(); } // #272281: the proprietary unrar program does not like trailing '/'s // in directories passed to it when extracting only part of // the files in an archive. QString CliPlugin::escapeFileName(const QString &fileName) const { if (fileName.endsWith(QLatin1Char('/'))) { return fileName.left(fileName.length() - 1); } return fileName; } ParameterList CliPlugin::parameterList() const { static ParameterList p; if (p.isEmpty()) { p[CaptureProgress] = true; p[ListProgram] = p[ExtractProgram] = QStringList() << QStringLiteral( "unrar" ); p[DeleteProgram] = p[AddProgram] = QStringList() << QStringLiteral( "rar" ); p[ListArgs] = QStringList() << QStringLiteral("vt") << QStringLiteral("-v") << QStringLiteral("$PasswordSwitch") << QStringLiteral("$Archive"); p[ExtractArgs] = QStringList() << QStringLiteral( "-kb" ) << QStringLiteral( "-p-" ) << QStringLiteral( "$PreservePathSwitch" ) << QStringLiteral( "$PasswordSwitch" ) << QStringLiteral( "$RootNodeSwitch" ) << QStringLiteral( "$Archive" ) << QStringLiteral( "$Files" ); p[PreservePathSwitch] = QStringList() << QStringLiteral( "x" ) << QStringLiteral( "e" ); p[RootNodeSwitch] = QStringList() << QStringLiteral( "-ap$Path" ); p[PasswordSwitch] = QStringList() << QStringLiteral( "-p$Password" ); p[PasswordHeaderSwitch] = QStringList() << QStringLiteral("-hp$Password"); p[CompressionLevelSwitch] = QStringLiteral("-m$CompressionLevel"); p[DeleteArgs] = QStringList() << QStringLiteral( "d" ) << QStringLiteral( "$Archive" ) << QStringLiteral( "$Files" ); p[FileExistsExpression] = QStringList() << QStringLiteral("^\\[Y\\]es, \\[N\\]o, \\[A\\]ll, n\\[E\\]ver, \\[R\\]ename, \\[Q\\]uit $"); p[FileExistsFileName] = QStringList() << QStringLiteral("^(.+) already exists. Overwrite it") // unrar 3 & 4 << QStringLiteral("^Would you like to replace the existing file (.+)$"); // unrar 5 p[FileExistsInput] = QStringList() << QStringLiteral( "Y" ) //overwrite << QStringLiteral( "N" ) //skip << QStringLiteral( "A" ) //overwrite all << QStringLiteral( "E" ) //autoskip << QStringLiteral( "Q" ); //cancel p[AddArgs] = QStringList() << QStringLiteral( "a" ) << QStringLiteral( "$Archive" ) << QStringLiteral("$PasswordSwitch") << QStringLiteral("$CompressionLevelSwitch") << QStringLiteral( "$Files" ); p[PasswordPromptPattern] = QLatin1String("Enter password \\(will not be echoed\\) for"); p[WrongPasswordPatterns] = QStringList() << QStringLiteral("password incorrect") << QStringLiteral("wrong password"); p[ExtractionFailedPatterns] = QStringList() << QStringLiteral( "CRC failed" ) << QStringLiteral( "Cannot find volume" ); p[CorruptArchivePatterns] = QStringList() << QStringLiteral("Unexpected end of archive") << QStringLiteral("the file header is corrupt"); p[DiskFullPatterns] = QStringList() << QStringLiteral("No space left on device"); + p[CommentArgs] = QStringList() << QStringLiteral("c") + << QStringLiteral("$CommentSwitch") + << QStringLiteral("$Archive"); + p[CommentSwitch] = QStringLiteral("-z$CommentFile"); } return p; } bool CliPlugin::readListLine(const QString &line) { // Ignore number of lines corresponding to m_remainingIgnoreLines. if (m_remainingIgnoreLines > 0) { --m_remainingIgnoreLines; return true; } // Parse the title line, which contains the version of unrar. if (m_parseState == ParseStateTitle) { QRegularExpression rxVersionLine(QStringLiteral("^UNRAR (\\d+\\.\\d+)( beta \\d)? .*$")); QRegularExpressionMatch matchVersion = rxVersionLine.match(line); if (matchVersion.hasMatch()) { m_parseState = ParseStateComment; QString unrarVersion = matchVersion.captured(1); qCDebug(ARK) << "UNRAR version" << unrarVersion << "detected"; if (unrarVersion.toFloat() >= 5) { m_isUnrar5 = true; qCDebug(ARK) << "Using UNRAR 5 parser"; } else { qCDebug(ARK) << "Using UNRAR 4 parser"; } } else { // If the second line doesn't contain an UNRAR title, something // is wrong. qCCritical(ARK) << "Failed to detect UNRAR output."; return false; } // Or see what version of unrar we are dealing with and call specific // handler functions. } else if (m_isUnrar5) { handleUnrar5Line(line); } else { handleUnrar4Line(line); } return true; } void CliPlugin::handleUnrar5Line(const QString &line) { // Parses the comment field. if (m_parseState == ParseStateComment) { // RegExp matching end of comment field. // FIXME: Comment itself could also contain the Archive path string here. QRegularExpression rxCommentEnd(QStringLiteral("^Archive: .+$")); if (rxCommentEnd.match(line).hasMatch()) { m_parseState = ParseStateHeader; m_comment = m_comment.trimmed(); m_linesComment = m_comment.count(QLatin1Char('\n')) + 1; if (!m_comment.isEmpty()) { qCDebug(ARK) << "Found a comment with" << m_linesComment << "lines"; } } else { m_comment.append(line + QLatin1Char('\n')); } return; } // Parses the header, which is whatever is between the comment field // and the entries. else if (m_parseState == ParseStateHeader) { // "Details: " indicates end of header. if (line.startsWith(QStringLiteral("Details: "))) { ignoreLines(1, ParseStateEntryDetails); if (line.contains(QLatin1String("volume")) && !m_isMultiVolume) { m_isMultiVolume = true; qCDebug(ARK) << "Multi-volume archive detected"; } if (line.contains(QLatin1String("solid")) && !m_isSolid) { m_isSolid = true; qCDebug(ARK) << "Solid archive detected"; } } return; } // Parses the entry details for each entry. else if (m_parseState == ParseStateEntryDetails) { // For multi-volume archives there is a header between the entries in // each volume. if (line.startsWith(QLatin1String("Archive: "))) { m_parseState = ParseStateHeader; return; // Empty line indicates end of entry. } else if (line.trimmed().isEmpty() && !m_unrar5Details.isEmpty()) { handleUnrar5Entry(); } else { // All detail lines should contain a colon. if (!line.contains(QLatin1Char(':'))) { qCWarning(ARK) << "Unrecognized line:" << line; return; } // The details are on separate lines, so we store them in the QHash // m_unrar5Details. m_unrar5Details.insert(line.section(QLatin1Char(':'), 0, 0).trimmed().toLower(), line.section(QLatin1Char(':'), 1).trimmed()); } return; } } void CliPlugin::handleUnrar5Entry() { ArchiveEntry e; QString compressionRatio = m_unrar5Details.value(QStringLiteral("ratio")); compressionRatio.chop(1); // Remove the '%' e[Ratio] = compressionRatio; QString time = m_unrar5Details.value(QStringLiteral("mtime")); QDateTime ts = QDateTime::fromString(time, QStringLiteral("yyyy-MM-dd HH:mm:ss,zzz")); e[Timestamp] = ts; bool isDirectory = (m_unrar5Details.value(QStringLiteral("type")) == QLatin1String("Directory")); e[IsDirectory] = isDirectory; if (isDirectory && !m_unrar5Details.value(QStringLiteral("name")).endsWith(QLatin1Char('/'))) { m_unrar5Details[QStringLiteral("name")] += QLatin1Char('/'); } QString compression = m_unrar5Details.value(QStringLiteral("compression")); int optionPos = compression.indexOf(QLatin1Char('-')); if (optionPos != -1) { e[Method] = compression.mid(optionPos); e[Version] = compression.left(optionPos).trimmed(); } else { // No method specified. e[Method].clear(); e[Version] = compression; } m_isPasswordProtected = m_unrar5Details.value(QStringLiteral("flags")).contains(QStringLiteral("encrypted")); e[IsPasswordProtected] = m_isPasswordProtected; e[FileName] = m_unrar5Details.value(QStringLiteral("name")); e[InternalID] = m_unrar5Details.value(QStringLiteral("name")); e[Size] = m_unrar5Details.value(QStringLiteral("size")); e[CompressedSize] = m_unrar5Details.value(QStringLiteral("packed size")); e[Permissions] = m_unrar5Details.value(QStringLiteral("attributes")); e[CRC] = m_unrar5Details.value(QStringLiteral("crc32")); if (e[Permissions].toString().startsWith(QLatin1Char('l'))) { e[Link] = m_unrar5Details.value(QStringLiteral("target")); } m_unrar5Details.clear(); emit entry(e); } void CliPlugin::handleUnrar4Line(const QString &line) { // Parses the comment field. if (m_parseState == ParseStateComment) { // RegExp matching end of comment field. // FIXME: Comment itself could also contain the Archive path string here. QRegularExpression rxCommentEnd(QStringLiteral("^(Solid archive|Archive|Volume) .+$")); if (rxCommentEnd.match(line).hasMatch()) { if (line.startsWith(QLatin1String("Volume")) && !m_isMultiVolume) { m_isMultiVolume = true; qCDebug(ARK) << "Multi-volume archive detected"; } if (line.startsWith(QLatin1String("Solid archive")) && !m_isSolid) { m_isSolid = true; qCDebug(ARK) << "Solid archive detected"; } m_parseState = ParseStateHeader; m_comment = m_comment.trimmed(); m_linesComment = m_comment.count(QLatin1Char('\n')) + 1; if (!m_comment.isEmpty()) { qCDebug(ARK) << "Found a comment with" << m_linesComment << "lines"; } } else { m_comment.append(line + QLatin1Char('\n')); } return; } // Parses the header, which is whatever is between the comment field // and the entries. else if (m_parseState == ParseStateHeader) { // Horizontal line indicates end of header. if (line.startsWith(QStringLiteral("--------------------"))) { m_parseState = ParseStateEntryFileName; } return; } // Parses the entry name, which is on the first line of each entry. else if (m_parseState == ParseStateEntryFileName) { // Ignore empty lines. if (line.trimmed().isEmpty()) { return; } // Three types of subHeaders can be displayed for unrar 3 and 4. // STM has 4 lines, RR has 3, and CMT has lines corresponding to // length of comment field +3. We ignore the subheaders. QRegularExpression rxSubHeader(QStringLiteral("^Data header type: (CMT|STM|RR)$")); QRegularExpressionMatch matchSubHeader = rxSubHeader.match(line); if (matchSubHeader.hasMatch()) { qCDebug(ARK) << "SubHeader of type" << matchSubHeader.captured(1) << "found"; if (matchSubHeader.captured(1) == QLatin1String("STM")) { ignoreLines(4, ParseStateEntryFileName); } else if (matchSubHeader.captured(1) == QLatin1String("CMT")) { ignoreLines(m_linesComment + 3, ParseStateEntryFileName); } else if (matchSubHeader.captured(1) == QLatin1String("RR")) { ignoreLines(3, ParseStateEntryFileName); } return; } // The entries list ends with a horizontal line, followed by a // single summary line or, for multi-volume archives, another header. if (line.startsWith(QStringLiteral("-----------------"))) { m_parseState = ParseStateHeader; return; // Encrypted files are marked with an asterisk. } else if (line.startsWith(QLatin1Char('*'))) { m_isPasswordProtected = true; m_unrar4Details.append(QString(line.trimmed()).remove(0, 1)); //Remove the asterisk // Entry names always start at the second position, so a line not // starting with a space is not an entry name. } else if (!line.startsWith(QLatin1Char(' '))) { qCWarning(ARK) << "Unrecognized line:" << line; return; // If we reach this, then we can assume the line is an entry name, so // save it, and move on to the rest of the entry details. } else { m_unrar4Details.append(line.trimmed()); } m_parseState = ParseStateEntryDetails; return; } // Parses the remainder of the entry details for each entry. else if (m_parseState == ParseStateEntryDetails) { // If the line following an entry name is empty, we did something // wrong. Q_ASSERT(!line.trimmed().isEmpty()); // If we reach a horizontal line, then the previous line was not an // entry name, so go back to header. if (line.startsWith(QStringLiteral("-----------------"))) { m_parseState = ParseStateHeader; return; } // In unrar 3 and 4 the details are on a single line, so we // pass a QStringList containing the details. We need to store // it due to symlinks (see below). m_unrar4Details.append(line.split(QLatin1Char(' '), QString::SkipEmptyParts)); // The details line contains 9 fields, so m_unrar4Details // should now contain 9 + the filename = 10 strings. If not, this is // not an archive entry. if (m_unrar4Details.size() != 10) { m_parseState = ParseStateHeader; return; } // When unrar 3 and 4 list a symlink, they output an extra line // containing the link target. The extra line is output after // the line we ignore, so we first need to ignore one line. if (m_unrar4Details.at(6).startsWith(QLatin1Char('l'))) { ignoreLines(1, ParseStateLinkTarget); return; } else { handleUnrar4Entry(); } // Unrar 3 & 4 show a third line for each entry, which contains // three details: Host OS, Solid, and Old. We can ignore this // line. ignoreLines(1, ParseStateEntryFileName); return; } // Parses a symlink target. else if (m_parseState == ParseStateLinkTarget) { m_unrar4Details.append(QString(line).remove(QStringLiteral("-->")).trimmed()); handleUnrar4Entry(); m_parseState = ParseStateEntryFileName; return; } } void CliPlugin::handleUnrar4Entry() { ArchiveEntry e; QDateTime ts = QDateTime::fromString(QString(m_unrar4Details.at(4) + QLatin1Char(' ') + m_unrar4Details.at(5)), QStringLiteral("dd-MM-yy hh:mm")); // Unrar 3 & 4 output dates with a 2-digit year but QDateTime takes it as // 19??. Let's take 1950 as cut-off; similar to KDateTime. if (ts.date().year() < 1950) { ts = ts.addYears(100); } e[Timestamp] = ts; bool isDirectory = ((m_unrar4Details.at(6).at(0) == QLatin1Char('d')) || (m_unrar4Details.at(6).at(1) == QLatin1Char('D'))); e[IsDirectory] = isDirectory; if (isDirectory && !m_unrar4Details.at(0).endsWith(QLatin1Char('/'))) { m_unrar4Details[0] += QLatin1Char('/'); } // Unrar reports the ratio as ((compressed size * 100) / size); // we consider ratio as (100 * ((size - compressed size) / size)). // If the archive is a multivolume archive, a string indicating // whether the archive's position in the volume is displayed // instead of the compression ratio. QString compressionRatio = m_unrar4Details.at(3); if ((compressionRatio == QStringLiteral("<--")) || (compressionRatio == QStringLiteral("<->")) || (compressionRatio == QStringLiteral("-->"))) { compressionRatio = QLatin1Char('0'); } else { compressionRatio.chop(1); // Remove the '%' } e[Ratio] = compressionRatio; // TODO: // - Permissions differ depending on the system the entry was added // to the archive. e[FileName] = m_unrar4Details.at(0); e[InternalID] = m_unrar4Details.at(0); e[Size] = m_unrar4Details.at(1); e[CompressedSize] = m_unrar4Details.at(2); e[Permissions] = m_unrar4Details.at(6); e[CRC] = m_unrar4Details.at(7); e[Method] = m_unrar4Details.at(8); e[Version] = m_unrar4Details.at(9); e[IsPasswordProtected] = m_isPasswordProtected; if (e[Permissions].toString().startsWith(QLatin1Char('l'))) { e[Link] = m_unrar4Details.at(10); } m_unrar4Details.clear(); emit entry(e); } void CliPlugin::ignoreLines(int lines, ParseState nextState) { m_remainingIgnoreLines = lines; m_parseState = nextState; } #include "cliplugin.moc" diff --git a/plugins/clirarplugin/kerfuffle_clirar.json b/plugins/clirarplugin/kerfuffle_clirar.json index 671d575c..744be89b 100644 --- a/plugins/clirarplugin/kerfuffle_clirar.json +++ b/plugins/clirarplugin/kerfuffle_clirar.json @@ -1,68 +1,69 @@ { "KPlugin": { "Authors": [ { "Email": "haraldhv@stud.ntnu.no", "Name": "Harald Hvaal", "Name[sr@ijekavian]": "Харалд Вол", "Name[sr@ijekavianlatin]": "Harald Vol", "Name[sr@latin]": "Harald Vol", "Name[sr]": "Харалд Вол", "Name[x-test]": "xxHarald Hvaalxx" } ], "Id": "kerfuffle_clirar", "License": "GPLv2+", "MimeTypes": [ "application/x-rar" ], "Name": "RAR archive plugin", "Name[ca@valencia]": "Connector per arxius RAR", "Name[ca]": "Connector per arxius RAR", "Name[cs]": "Modul pro archiv RAR", "Name[de]": "RAR-Archiv-Modul", "Name[en_GB]": "RAR archive plugin", "Name[es]": "Complemento de archivo RAR", "Name[fi]": "RAR-pakkaustuki", "Name[fr]": "Module externe d'archive « RAR »", "Name[it]": "Estensione per archivi RAR", "Name[nb]": "Programtillegg for RAR-arkiv", "Name[nl]": "RAR-archiefplug-in", "Name[nn]": "RAR-arkivtillegg", "Name[pl]": "Wtyczka archiwów RAR", "Name[pt]": "'Plugin' de pacotes RAR", "Name[pt_BR]": "Plugin de arquivos RAR", "Name[ru]": "Поддержка архивов RAR", "Name[sk]": "Modul RAR archívu", "Name[sl]": "Vstavek za arhive RAR", "Name[sr@ijekavian]": "Прикључак РАР архива", "Name[sr@ijekavianlatin]": "Priključak RAR arhiva", "Name[sr@latin]": "Priključak RAR arhiva", "Name[sr]": "Прикључак РАР архива", "Name[sv]": "Insticksprogram för RAR-arkiv", "Name[uk]": "Додаток для архівів RAR", "Name[x-test]": "xxRAR archive pluginxx", "Name[zh_CN]": "RAR 归档插件", "Name[zh_TW]": "RAR 壓縮檔外掛程式", "ServiceTypes": [ "Kerfuffle/Plugin" ], "Version": "0.0.1", "Website": "http://www.kde.org" }, "X-KDE-Kerfuffle-APIRevision": 1, "X-KDE-Kerfuffle-ReadOnlyExecutables": [ "unrar" ], "X-KDE-Kerfuffle-ReadWrite": true, "X-KDE-Kerfuffle-ReadWriteExecutables": [ "rar" ], "X-KDE-Priority": 120, "application/x-rar": { "CompressionLevelDefault": 3, "CompressionLevelMax": 5, - "CompressionLevelMin": 0, + "CompressionLevelMin": 0, + "SupportsWriteComment": true, "HeaderEncryption": true } -} \ No newline at end of file +} diff --git a/plugins/libarchive/libarchiveplugin.cpp b/plugins/libarchive/libarchiveplugin.cpp index 5c4e3912..25246f2d 100644 --- a/plugins/libarchive/libarchiveplugin.cpp +++ b/plugins/libarchive/libarchiveplugin.cpp @@ -1,527 +1,533 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2010 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "libarchiveplugin.h" #include "ark_debug.h" #include "kerfuffle/kerfuffle_export.h" #include "kerfuffle/queries.h" #include #include #include #include #include #include #include LibarchivePlugin::LibarchivePlugin(QObject *parent, const QVariantList & args) : ReadWriteArchiveInterface(parent, args) , m_archiveReadDisk(archive_read_disk_new()) , m_abortOperation(false) , m_cachedArchiveEntryCount(0) , m_emitNoEntries(false) , m_extractedFilesSize(0) { qCDebug(ARK) << "Initializing libarchive plugin"; archive_read_disk_set_standard_lookup(m_archiveReadDisk.data()); } LibarchivePlugin::~LibarchivePlugin() { } bool LibarchivePlugin::list() { qCDebug(ARK) << "Listing archive contents"; ArchiveRead arch_reader(archive_read_new()); if (!(arch_reader.data())) { return false; } if (archive_read_support_filter_all(arch_reader.data()) != ARCHIVE_OK) { return false; } if (archive_read_support_format_all(arch_reader.data()) != ARCHIVE_OK) { return false; } if (archive_read_open_filename(arch_reader.data(), QFile::encodeName(filename()), 10240) != ARCHIVE_OK) { emit error(i18nc("@info", "Could not open the archive.")); return false; } qDebug(ARK) << "Detected compression filter:" << archive_filter_name(arch_reader.data(), 0); m_cachedArchiveEntryCount = 0; m_extractedFilesSize = 0; struct archive_entry *aentry; int result; bool firstEntry = true; while (!m_abortOperation && (result = archive_read_next_header(arch_reader.data(), &aentry)) == ARCHIVE_OK) { if (firstEntry) { qDebug(ARK) << "Detected format for first entry:" << archive_format_name(arch_reader.data()); firstEntry = false; } if (!m_emitNoEntries) { emitEntryFromArchiveEntry(aentry); } m_extractedFilesSize += (qlonglong)archive_entry_size(aentry); m_cachedArchiveEntryCount++; archive_read_data_skip(arch_reader.data()); } m_abortOperation = false; if (result != ARCHIVE_EOF) { const QString errorString = QLatin1String(archive_error_string(arch_reader.data())); // FIXME: what about the other archive_error_string() calls? Do they also happen to return empty strings? emit error(errorString.isEmpty() ? i18nc("@info", "Could not read until the end of the archive") : errorString); return false; } return archive_read_close(arch_reader.data()) == ARCHIVE_OK; } bool LibarchivePlugin::addFiles(const QStringList &files, const CompressionOptions &options) { Q_UNUSED(files) Q_UNUSED(options) return false; } bool LibarchivePlugin::deleteFiles(const QList &files) { Q_UNUSED(files) return false; } +bool LibarchivePlugin::addComment(const QString& comment) +{ + Q_UNUSED(comment) + return false; +} + bool LibarchivePlugin::doKill() { m_abortOperation = true; return true; } bool LibarchivePlugin::copyFiles(const QVariantList& files, const QString& destinationDirectory, const ExtractionOptions& options) { qCDebug(ARK) << "Changing current directory to " << destinationDirectory; QDir::setCurrent(destinationDirectory); const bool extractAll = files.isEmpty(); const bool preservePaths = options.value(QStringLiteral( "PreservePaths" )).toBool(); bool removeRootNode = options.value(QStringLiteral("RemoveRootNode"), QVariant()).toBool(); // See if there is a singular RootNode. QString rootNodeSingular = options.value(QStringLiteral("RootNode"), QVariant()).toString(); if (!rootNodeSingular.isEmpty() && !rootNodeSingular.endsWith(QLatin1Char('/'))) { rootNodeSingular.append(QLatin1Char('/')); } // To avoid traversing the entire archive when extracting a limited set of // entries, we maintain a list of remaining entries and stop when it's // empty. QVariantList remainingFiles = files; ArchiveRead arch(archive_read_new()); if (!(arch.data())) { return false; } if (archive_read_support_filter_all(arch.data()) != ARCHIVE_OK) { return false; } if (archive_read_support_format_all(arch.data()) != ARCHIVE_OK) { return false; } if (archive_read_open_filename(arch.data(), QFile::encodeName(filename()), 10240) != ARCHIVE_OK) { // This error might be shown outside of a running Ark part (e.g. from a batch job). emit error(xi18nc("@info", "Could not open the archive %1." "Check whether you have sufficient permissions.", filename())); return false; } ArchiveWrite writer(archive_write_disk_new()); if (!writer.data()) { return false; } archive_write_disk_set_options(writer.data(), extractionFlags()); int entryNr = 0; int totalCount = 0; if (extractAll) { if (!m_cachedArchiveEntryCount) { emit progress(0); //TODO: once information progress has been implemented, send //feedback here that the archive is being read qCDebug(ARK) << "For getting progress information, the archive will be listed once"; m_emitNoEntries = true; list(); m_emitNoEntries = false; } totalCount = m_cachedArchiveEntryCount; } else { totalCount = files.size(); } qCDebug(ARK) << "Going to extract" << totalCount << "entries"; // Initialize variables. bool overwriteAll = false; // Whether to overwrite all files bool skipAll = false; // Whether to skip all files bool dontPromptErrors = false; // Whether to prompt for errors m_currentExtractedFilesSize = 0; int no_entries = 0; struct archive_entry *entry; QString fileBeingRenamed; // Iterate through all entries in archive. while (!m_abortOperation && (archive_read_next_header(arch.data(), &entry) == ARCHIVE_OK)) { if (!extractAll && remainingFiles.isEmpty()) { break; } fileBeingRenamed.clear(); int index; // Retry with renamed entry, fire an overwrite query again // if the new entry also exists. retry: const bool entryIsDir = S_ISDIR(archive_entry_mode(entry)); // Skip directories if not preserving paths. if (!preservePaths && entryIsDir) { archive_read_data_skip(arch.data()); continue; } // entryName is the name inside the archive, full path QString entryName = QDir::fromNativeSeparators(QFile::decodeName(archive_entry_pathname(entry))); // For now we just can't handle absolute filenames in a tar archive. // TODO: find out what to do here!! if (entryName.startsWith(QLatin1Char( '/' ))) { emit error(i18n("This archive contains archive entries with absolute paths, " "which are not supported by Ark.")); return false; } // Should the entry be extracted? if (extractAll || remainingFiles.contains(QVariant::fromValue(fileRootNodePair(entryName))) || entryName == fileBeingRenamed) { // Find the index of entry. if (entryName != fileBeingRenamed) { index = files.indexOf(QVariant::fromValue(fileRootNodePair(entryName))); } if (!extractAll && index == -1) { // If entry is not found in files, skip entry. continue; } // entryFI is the fileinfo pointing to where the file will be // written from the archive. QFileInfo entryFI(entryName); //qCDebug(ARK) << "setting path to " << archive_entry_pathname( entry ); const QString fileWithoutPath(entryFI.fileName()); // If we DON'T preserve paths, we cut the path and set the entryFI // fileinfo to the one without the path. if (!preservePaths) { // Empty filenames (ie dirs) should have been skipped already, // so asserting. Q_ASSERT(!fileWithoutPath.isEmpty()); archive_entry_copy_pathname(entry, QFile::encodeName(fileWithoutPath).constData()); entryFI = QFileInfo(fileWithoutPath); // OR, if the file has a rootNode attached, remove it from file path. } else if (!extractAll && removeRootNode && entryName != fileBeingRenamed && !files.at(index).value().rootNode.isEmpty()) { //qCDebug(ARK) << "Removing" << files.at(index).value().rootNode << "from" << entryName; const QString truncatedFilename(entryName.remove(0, files.at(index).value().rootNode.size())); archive_entry_copy_pathname(entry, QFile::encodeName(truncatedFilename).constData()); entryFI = QFileInfo(truncatedFilename); // OR, if a singular rootNode is provided, remove it from file path. } else if (removeRootNode && entryName != fileBeingRenamed && !rootNodeSingular.isEmpty()) { //qCDebug(ARK) << "Removing" << rootNodeSingular << "from" << entryName; const QString truncatedFilename(entryName.remove(0, rootNodeSingular.size())); archive_entry_copy_pathname(entry, QFile::encodeName(truncatedFilename).constData()); entryFI = QFileInfo(truncatedFilename); } // Check if the file about to be written already exists. if (!entryIsDir && entryFI.exists()) { if (skipAll) { archive_read_data_skip(arch.data()); archive_entry_clear(entry); continue; } else if (!overwriteAll && !skipAll) { Kerfuffle::OverwriteQuery query(entryName); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { archive_read_data_skip(arch.data()); archive_entry_clear(entry); break; } else if (query.responseSkip()) { archive_read_data_skip(arch.data()); archive_entry_clear(entry); continue; } else if (query.responseAutoSkip()) { archive_read_data_skip(arch.data()); archive_entry_clear(entry); skipAll = true; continue; } else if (query.responseRename()) { const QString newName(query.newFilename()); fileBeingRenamed = newName; archive_entry_copy_pathname(entry, QFile::encodeName(newName).constData()); goto retry; } else if (query.responseOverwriteAll()) { overwriteAll = true; } } } // If there is an already existing directory. if (entryIsDir && entryFI.exists()) { if (entryFI.isWritable()) { qCWarning(ARK) << "Warning, existing, but writable dir"; } else { qCWarning(ARK) << "Warning, existing, but non-writable dir. skipping"; archive_entry_clear(entry); archive_read_data_skip(arch.data()); continue; } } // Write the entry header and check return value. const int returnCode = archive_write_header(writer.data(), entry); switch (returnCode) { case ARCHIVE_OK: // If the whole archive is extracted and the total filesize is // available, we use partial progress. copyData(entryName, arch.data(), writer.data(), (extractAll && m_extractedFilesSize)); break; case ARCHIVE_FAILED: qCCritical(ARK) << "archive_write_header() has returned" << returnCode << "with errno" << archive_errno(writer.data()); // If they user previously decided to ignore future errors, // don't bother prompting again. if (!dontPromptErrors) { // Ask the user if he wants to continue extraction despite an error for this entry. Kerfuffle::ContinueExtractionQuery query(QLatin1String(archive_error_string(writer.data())), entryName); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); return false; } dontPromptErrors = query.dontAskAgain(); } break; case ARCHIVE_FATAL: qCCritical(ARK) << "archive_write_header() has returned" << returnCode << "with errno" << archive_errno(writer.data()); emit error(xi18nc("@info", "Extraction failed at:%1", entryName)); return false; default: qCDebug(ARK) << "archive_write_header() returned" << returnCode << "which will be ignored."; break; } // If we only partially extract the archive and the number of // archive entries is available we use a simple progress based on // number of items extracted. if (!extractAll && m_cachedArchiveEntryCount) { ++entryNr; emit progress(float(entryNr) / totalCount); } archive_entry_clear(entry); no_entries++; remainingFiles.removeOne(QVariant::fromValue(fileRootNodePair(entryName))); } else { // Archive entry not among selected files, skip it. archive_read_data_skip(arch.data()); } } // While entries left to read in archive. m_abortOperation = false; qCDebug(ARK) << "Extracted" << no_entries << "entries"; return archive_read_close(arch.data()) == ARCHIVE_OK; } void LibarchivePlugin::emitEntryFromArchiveEntry(struct archive_entry *aentry) { ArchiveEntry e; #ifdef _MSC_VER e[FileName] = QDir::fromNativeSeparators(QString::fromUtf16((ushort*)archive_entry_pathname_w(aentry))); #else e[FileName] = QDir::fromNativeSeparators(QString::fromWCharArray(archive_entry_pathname_w(aentry))); #endif e[InternalID] = e[FileName]; const QString owner = QString::fromLatin1(archive_entry_uname(aentry)); if (!owner.isEmpty()) { e[Owner] = owner; } const QString group = QString::fromLatin1(archive_entry_gname(aentry)); if (!group.isEmpty()) { e[Group] = group; } e[Size] = (qlonglong)archive_entry_size(aentry); e[IsDirectory] = S_ISDIR(archive_entry_mode(aentry)); if (archive_entry_symlink(aentry)) { e[Link] = QLatin1String( archive_entry_symlink(aentry) ); } e[Timestamp] = QDateTime::fromTime_t(archive_entry_mtime(aentry)); emit entry(e); } int LibarchivePlugin::extractionFlags() const { int result = ARCHIVE_EXTRACT_TIME; result |= ARCHIVE_EXTRACT_SECURE_NODOTDOT; // TODO: Don't use arksettings here /*if ( ArkSettings::preservePerms() ) { result &= ARCHIVE_EXTRACT_PERM; } if ( !ArkSettings::extractOverwrite() ) { result &= ARCHIVE_EXTRACT_NO_OVERWRITE; }*/ return result; } void LibarchivePlugin::copyData(const QString& filename, struct archive *dest, bool partialprogress) { char buff[10240]; ssize_t readBytes; QFile file(filename); if (!file.open(QIODevice::ReadOnly)) { return; } readBytes = file.read(buff, sizeof(buff)); while (readBytes > 0) { archive_write_data(dest, buff, readBytes); if (archive_errno(dest) != ARCHIVE_OK) { qCCritical(ARK) << "Error while writing" << filename << ":" << archive_error_string(dest) << "(error no =" << archive_errno(dest) << ')'; return; } if (partialprogress) { m_currentExtractedFilesSize += readBytes; emit progress(float(m_currentExtractedFilesSize) / m_extractedFilesSize); } readBytes = file.read(buff, sizeof(buff)); } file.close(); } void LibarchivePlugin::copyData(const QString& filename, struct archive *source, struct archive *dest, bool partialprogress) { char buff[10240]; ssize_t readBytes; readBytes = archive_read_data(source, buff, sizeof(buff)); while (readBytes > 0) { archive_write_data(dest, buff, readBytes); if (archive_errno(dest) != ARCHIVE_OK) { qCCritical(ARK) << "Error while extracting" << filename << ":" << archive_error_string(dest) << "(error no =" << archive_errno(dest) << ')'; return; } if (partialprogress) { m_currentExtractedFilesSize += readBytes; emit progress(float(m_currentExtractedFilesSize) / m_extractedFilesSize); } readBytes = archive_read_data(source, buff, sizeof(buff)); } } #include "libarchiveplugin.moc" diff --git a/plugins/libarchive/libarchiveplugin.h b/plugins/libarchive/libarchiveplugin.h index 91d66e9d..94849e40 100644 --- a/plugins/libarchive/libarchiveplugin.h +++ b/plugins/libarchive/libarchiveplugin.h @@ -1,93 +1,94 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef LIBARCHIVEPLUGIN_H #define LIBARCHIVEPLUGIN_H #include "kerfuffle/archiveinterface.h" #include #include using namespace Kerfuffle; class LibarchivePlugin : public ReadWriteArchiveInterface { Q_OBJECT public: explicit LibarchivePlugin(QObject *parent, const QVariantList& args); virtual ~LibarchivePlugin(); virtual bool list() Q_DECL_OVERRIDE; virtual bool doKill() Q_DECL_OVERRIDE; virtual bool copyFiles(const QVariantList& files, const QString& destinationDirectory, const ExtractionOptions& options) Q_DECL_OVERRIDE; virtual bool addFiles(const QStringList& files, const CompressionOptions& options) Q_DECL_OVERRIDE; virtual bool deleteFiles(const QList& files) Q_DECL_OVERRIDE; + virtual bool addComment(const QString& comment) Q_DECL_OVERRIDE; protected: void emitEntryFromArchiveEntry(struct archive_entry *entry); void copyData(const QString& filename, struct archive *dest, bool partialprogress = true); void copyData(const QString& filename, struct archive *source, struct archive *dest, bool partialprogress = true); struct ArchiveReadCustomDeleter { static inline void cleanup(struct archive *a) { if (a) { archive_read_free(a); } } }; struct ArchiveWriteCustomDeleter { static inline void cleanup(struct archive *a) { if (a) { archive_write_free(a); } } }; typedef QScopedPointer ArchiveRead; typedef QScopedPointer ArchiveWrite; ArchiveRead m_archiveReadDisk; bool m_abortOperation; private: int extractionFlags() const; int m_cachedArchiveEntryCount; qlonglong m_currentExtractedFilesSize; bool m_emitNoEntries; qlonglong m_extractedFilesSize; }; #endif // LIBARCHIVEPLUGIN_H