[ark/gsoc2016/master] /: [GSoC] Merge master into gsoc2016/master

Vladyslav Batyrenko mvlabat at gmail.com
Mon Aug 15 17:05:24 UTC 2016


Git commit 7eb3304db549df12afca7e25698cdf369e6e4a7e by Vladyslav Batyrenko.
Committed on 15/08/2016 at 17:04.
Pushed by vbatyrenko into branch 'gsoc2016/master'.

[GSoC] Merge master into gsoc2016/master

M  +3    -3    app/batchextract.cpp
M  +3    -1    app/org.kde.ark.desktop.cmake
M  +1    -0    autotests/CMakeLists.txt
M  +105  -8    autotests/kerfuffle/addtoarchivetest.cpp
R  +65   -18   autotests/kerfuffle/extracttest.cpp
M  +5    -0    autotests/kerfuffle/jobstest.cpp
M  +46   -10   autotests/plugins/cli7zplugin/cli7ztest.cpp
M  +47   -15   autotests/plugins/clirarplugin/clirartest.cpp
M  +7    -1    autotests/plugins/cliunarchiverplugin/cliunarchivertest.cpp
M  +1    -1    autotests/plugins/clizipplugin/cliziptest.cpp
A  +-    --    doc/create-archive.png
M  +-    --    doc/create-protected-archive.png
M  +48   -4    doc/index.docbook
M  +16   -3    doc/man-ark.1.docbook
M  +3    -3    kerfuffle/addtoarchive.cpp
M  +65   -9    kerfuffle/archive_kerfuffle.cpp
M  +13   -2    kerfuffle/archive_kerfuffle.h
M  +22   -0    kerfuffle/archiveinterface.cpp
M  +7    -0    kerfuffle/archiveinterface.h
M  +86   -42   kerfuffle/cliinterface.cpp
M  +43   -28   kerfuffle/cliinterface.h
M  +42   -29   kerfuffle/jobs.cpp
M  +8    -17   kerfuffle/jobs.h
M  +84   -2    kerfuffle/mime/kerfuffle.xml
M  +73   -9    part/archivemodel.cpp
M  +13   -0    part/archivemodel.h
M  +67   -20   part/part.cpp
M  +2    -2    part/part.h
M  +11   -3    plugins/cli7zplugin/cliplugin.cpp
M  +23   -8    plugins/clirarplugin/cliplugin.cpp
M  +0    -1    plugins/clirarplugin/cliplugin.h
M  +85   -27   plugins/cliunarchiverplugin/cliplugin.cpp
M  +4    -1    plugins/cliunarchiverplugin/cliplugin.h
M  +2    -1    plugins/clizipplugin/cliplugin.cpp

http://commits.kde.org/ark/7eb3304db549df12afca7e25698cdf369e6e4a7e

diff --cc app/org.kde.ark.desktop.cmake
index c68523b,9f61e7d..e6bede5
--- a/app/org.kde.ark.desktop.cmake
+++ b/app/org.kde.ark.desktop.cmake
@@@ -178,6 -178,6 +178,7 @@@ Comment[nl]=Mert bestandsarchieven werk
  Comment[pl]=Pracuj z archiwami plików
  Comment[pt]=Lidar com pacotes em ficheiros
  Comment[pt_BR]=Manipulação de arquivos compactados
++Comment[ru]=Работа с архивами файлов
  Comment[sk]=Práca s archívmi súborov
  Comment[sl]=Delajte z datotečnimi arhivi
  Comment[sr]=Рад са фајловима архива
diff --cc autotests/CMakeLists.txt
index 2ea2753,71c7995..f477472
--- a/autotests/CMakeLists.txt
+++ b/autotests/CMakeLists.txt
@@@ -1,5 -1,5 +1,6 @@@
  include(ECMAddTests)
  
+ add_subdirectory(app)
 +add_subdirectory(testhelper)
  add_subdirectory(kerfuffle)
  add_subdirectory(plugins)
diff --cc autotests/kerfuffle/addtoarchivetest.cpp
index 410bb96,0613d6e..abaa0b4
--- a/autotests/kerfuffle/addtoarchivetest.cpp
+++ b/autotests/kerfuffle/addtoarchivetest.cpp
@@@ -38,11 -40,17 +40,18 @@@ class AddToArchiveTest : public QObjec
  
  private Q_SLOTS:
  
+     void init();
      void testCompressHere_data();
      void testCompressHere();
 +    void testCreateEncryptedArchive();
  };
  
+ void AddToArchiveTest::init()
+ {
+     // The test needs an empty subfolder, but git doesn't support tracking of empty directories.
+     QDir(QFINDTESTDATA("data/testdirwithemptysubdir")).mkdir(QStringLiteral("emptydir"));
+ }
+ 
  void AddToArchiveTest::testCompressHere_data()
  {
      QTest::addColumn<QString>("expectedSuffix");
diff --cc autotests/kerfuffle/extracttest.cpp
index ca18801,eae0ea8..02aac41
--- a/autotests/kerfuffle/extracttest.cpp
+++ b/autotests/kerfuffle/extracttest.cpp
@@@ -168,11 -171,29 +170,29 @@@ void ExtractTest::testProperties_data(
      QTest::newRow("mimetype child of application/zip")
              << QFINDTESTDATA("data/test.odt")
              << QStringLiteral("test")
-             << false << true << false << Archive::Unencrypted
+             << false << true << false << false << 0 << Archive::Unencrypted
              << QStringLiteral("test");
+ 
+     QTest::newRow("AppImage")
+             << QFINDTESTDATA("data/hello-1.0-x86_64.AppImage")
+             << QStringLiteral("hello-1.0-x86_64")
+             << true << false << false << false << 0 << Archive::Unencrypted
+             << QStringLiteral("hello-1.0-x86_64");
+ 
+     QTest::newRow("7z multivolume")
+             << QFINDTESTDATA("data/archive-multivolume.7z.001")
+             << QStringLiteral("archive-multivolume")
+             << true << false << false << true << 3 << Archive::Unencrypted
+             << QStringLiteral("archive-multivolume");
+ 
+     QTest::newRow("rar multivolume")
+             << QFINDTESTDATA("data/archive-multivolume.part1.rar")
+             << QStringLiteral("archive-multivolume")
+             << true << false << false << true << 3 << Archive::Unencrypted
+             << QStringLiteral("archive-multivolume");
  }
  
 -void ArchiveTest::testProperties()
 +void ExtractTest::testProperties()
  {
      QFETCH(QString, archivePath);
      Archive *archive = Archive::create(archivePath, this);
@@@ -506,12 -533,33 +532,33 @@@ void ExtractTest::testExtraction_data(
      archivePath = QFINDTESTDATA("data/simplearchive.xar");
      QTest::newRow("extract all entries from a xar archive with path")
              << archivePath
 -            << QVariantList()
 +            << QList<Archive::Entry*>()
              << optionsPreservePaths
              << 6;
+ 
 -    archivePath = QFINDTESTDATA("data/hello-2.8-x86_64.AppImage");
++    archivePath = QFINDTESTDATA("data/hello-1.0-x86_64.AppImage");
+     QTest::newRow("extract all entries from an AppImage with path")
+             << archivePath
 -            << QVariantList()
++            << QList<Archive::Entry*>()
+             << optionsPreservePaths
+             << 7;
+ 
+     archivePath = QFINDTESTDATA("data/archive-multivolume.7z.001");
+     QTest::newRow("extract all entries from a multivolume 7z archive with path")
+             << archivePath
 -            << QVariantList()
++            << QList<Archive::Entry*>()
+             << optionsPreservePaths
+             << 3;
+ 
+     archivePath = QFINDTESTDATA("data/archive-multivolume.part1.rar");
+     QTest::newRow("extract all entries from a multivolume rar archive with path")
+             << archivePath
 -            << QVariantList()
++            << QList<Archive::Entry*>()
+             << optionsPreservePaths
+             << 3;
  }
  
 -void ArchiveTest::testExtraction()
 +void ExtractTest::testExtraction()
  {
      QFETCH(QString, archivePath);
      Archive *archive = Archive::create(archivePath, this);
diff --cc autotests/kerfuffle/jobstest.cpp
index 6392d8e,f20bd90..073fe3f
--- a/autotests/kerfuffle/jobstest.cpp
+++ b/autotests/kerfuffle/jobstest.cpp
@@@ -253,8 -253,10 +253,10 @@@ void JobsTest::testExtractJobAccessors(
  void JobsTest::testTempExtractJob()
  {
      JSONArchiveInterface *iface = createArchiveInterface(QFINDTESTDATA("data/archive-malicious.json"));
 -    PreviewJob *job = new PreviewJob(QStringLiteral("anotherDir/../../file.txt"), false, iface);
 +    PreviewJob *job = new PreviewJob(new Archive::Entry(this, QStringLiteral("anotherDir/../../file.txt")), false, iface);
  
+     const QString tempDirPath = job->tempDir()->path();
+     QVERIFY(QFileInfo::exists(tempDirPath));
      QVERIFY(job->validatedFilePath().endsWith(QLatin1String("anotherDir/file.txt")));
      QVERIFY(job->extractionOptions()[QStringLiteral("PreservePaths")].toBool());
  
diff --cc autotests/plugins/cli7zplugin/cli7ztest.cpp
index 3cb3e4f,727214c..655980f
--- a/autotests/plugins/cli7zplugin/cli7ztest.cpp
+++ b/autotests/plugins/cli7zplugin/cli7ztest.cpp
@@@ -159,24 -176,30 +176,30 @@@ void Cli7zTest::testList(
  
      QCOMPARE(signalSpy.count(), expectedEntriesCount);
  
+     QFETCH(bool, isMultiVolume);
+     QCOMPARE(plugin->isMultiVolume(), isMultiVolume);
+ 
+     QFETCH(int, numberOfVolumes);
+     QCOMPARE(plugin->numberOfVolumes(), numberOfVolumes);
+ 
      QFETCH(int, someEntryIndex);
      QVERIFY(someEntryIndex < signalSpy.count());
 -    ArchiveEntry entry = qvariant_cast<ArchiveEntry>(signalSpy.at(someEntryIndex).at(0));
 +    Archive::Entry *entry = signalSpy.at(someEntryIndex).at(0).value<Archive::Entry*>();
  
      QFETCH(QString, expectedName);
 -    QCOMPARE(entry[FileName].toString(), expectedName);
 +    QCOMPARE(entry->fullPath(), expectedName);
  
      QFETCH(bool, isDirectory);
 -    QCOMPARE(entry[IsDirectory].toBool(), isDirectory);
 +    QCOMPARE(entry->isDir(), isDirectory);
  
      QFETCH(bool, isPasswordProtected);
 -    QCOMPARE(entry[IsPasswordProtected].toBool(), isPasswordProtected);
 +    QCOMPARE(entry->property("isPasswordProtected").toBool(), isPasswordProtected);
  
      QFETCH(qulonglong, expectedSize);
 -    QCOMPARE(entry[Size].toULongLong(), expectedSize);
 +    QCOMPARE(entry->property("size").toULongLong(), expectedSize);
  
      QFETCH(QString, expectedTimestamp);
 -    QCOMPARE(entry[Timestamp].toString(), expectedTimestamp);
 +    QCOMPARE(entry->property("timestamp").toString(), expectedTimestamp);
  
      plugin->deleteLater();
  }
diff --cc autotests/plugins/clirarplugin/clirartest.cpp
index 4640e8a,d2988d9..5775b2b
--- a/autotests/plugins/clirarplugin/clirartest.cpp
+++ b/autotests/plugins/clirarplugin/clirartest.cpp
@@@ -187,30 -200,36 +200,36 @@@ void CliRarTest::testList(
  
      QCOMPARE(signalSpy.count(), expectedEntriesCount);
  
+     QFETCH(bool, isMultiVolume);
+     QCOMPARE(rarPlugin->isMultiVolume(), isMultiVolume);
+ 
+     QFETCH(int, numberOfVolumes);
+     QCOMPARE(rarPlugin->numberOfVolumes(), numberOfVolumes);
+ 
      QFETCH(int, someEntryIndex);
      QVERIFY(someEntryIndex < signalSpy.count());
 -    ArchiveEntry entry = qvariant_cast<ArchiveEntry>(signalSpy.at(someEntryIndex).at(0));
 +    Archive::Entry *entry = signalSpy.at(someEntryIndex).at(0).value<Archive::Entry*>();
  
      QFETCH(QString, expectedName);
 -    QCOMPARE(entry[FileName].toString(), expectedName);
 +    QCOMPARE(entry->fullPath(), expectedName);
  
      QFETCH(bool, isDirectory);
 -    QCOMPARE(entry[IsDirectory].toBool(), isDirectory);
 +    QCOMPARE(entry->isDir(), isDirectory);
  
      QFETCH(bool, isPasswordProtected);
 -    QCOMPARE(entry[IsPasswordProtected].toBool(), isPasswordProtected);
 +    QCOMPARE(entry->property("isPasswordProtected").toBool(), isPasswordProtected);
  
      QFETCH(QString, symlinkTarget);
 -    QCOMPARE(entry[Link].toString(), symlinkTarget);
 +    QCOMPARE(entry->property("link").toString(), symlinkTarget);
  
      QFETCH(qulonglong, expectedSize);
 -    QCOMPARE(entry[Size].toULongLong(), expectedSize);
 +    QCOMPARE(entry->property("size").toULongLong(), expectedSize);
  
      QFETCH(qulonglong, expectedCompressedSize);
 -    QCOMPARE(entry[CompressedSize].toULongLong(), expectedCompressedSize);
 +    QCOMPARE(entry->property("compressedSize").toULongLong(), expectedCompressedSize);
  
      QFETCH(QString, expectedTimestamp);
 -    QCOMPARE(entry[Timestamp].toString(), expectedTimestamp);
 +    QCOMPARE(entry->property("timestamp").toString(), expectedTimestamp);
  
      rarPlugin->deleteLater();
  }
diff --cc autotests/plugins/cliunarchiverplugin/cliunarchivertest.cpp
index eb22cfe,3713cc4..2b2fd1d
--- a/autotests/plugins/cliunarchiverplugin/cliunarchivertest.cpp
+++ b/autotests/plugins/cliunarchiverplugin/cliunarchivertest.cpp
@@@ -273,9 -273,15 +273,15 @@@ void CliUnarchiverTest::testExtraction_
  
      QTest::newRow("rar with empty folders")
              << QFINDTESTDATA("data/empty_folders.rar")
 -            << QVariantList()
 +            << QList<Archive::Entry*>()
              << optionsPreservePaths
              << 5;
+ 
+     QTest::newRow("rar with hidden folder and files")
+             << QFINDTESTDATA("data/hidden_files.rar")
 -            << QVariantList()
++            << QList<Archive::Entry*>()
+             << optionsPreservePaths
+             << 4;
  }
  
  // TODO: we can remove this test (which is duplicated from kerfuffle/archivetest)
diff --cc doc/create-archive.png
index 0000000,0000000..40a6bf5
new file mode 100644
Binary files differ
diff --cc doc/create-protected-archive.png
index 686d701,686d701..cf3d5f2
Binary files differ
diff --cc doc/index.docbook
index 73edbcd,da20381..6b17457
--- a/doc/index.docbook
+++ b/doc/index.docbook
@@@ -45,7 -44,7 +44,7 @@@
  
  <legalnotice>&FDLNotice;</legalnotice>
  
--<date>2016-05-05</date>
++<date>2016-08-09</date>
  <releaseinfo>Applications 16.08</releaseinfo>
  
  <abstract>
@@@ -74,7 -73,7 +73,8 @@@ archives
  <command>tar</command>, <command>gzip</command>,
  <command>bzip2</command>, <command>zip</command>, <command>rar</command>, 
  <command>7zip</command>, <command>xz</command>, <command>rpm</command>, 
--<command>cab</command> and <command>deb</command> (support for certain archive formats depends on
++<command>cab</command>, <command>deb</command>, <command>xar</command>
++and <command>AppImage</command> (support for certain archive formats depends on
  the appropriate command-line programs being installed).</para>
  
  <mediaobject>
@@@ -121,6 -120,6 +121,10 @@@ For example, you can save the archive w
  Archive properties such as type, size and MD5 hash can be viewed using the
  <guimenuitem>Properties</guimenuitem> item.</para>
  
++<para>&ark; has the ability to test archives for integrity. This functionality is currently available for
++<command>zip</command>, <command>rar</command> and <command>7z</command> archives.
++The test action can be found in the <guimenu>Archive</guimenu> menu.</para>
++
  </sect2>
  
  <sect2 id="ark-archive-comments">
@@@ -263,11 -262,11 +267,20 @@@ archive.</para
  <guimenuitem>New</guimenuitem> (<keycombo action="simul">&Ctrl;<keycap>N</keycap></keycombo>)
  from the <guimenu>Archive</guimenu> menu.</para>
  
++<mediaobject>
++<imageobject>
++<imagedata fileref="create-archive.png" format="PNG"/>
++</imageobject>
++<textobject>
++<phrase>Create an archive</phrase>
++</textobject>
++</mediaobject>
++
  <para>You can then type the name of the archive, with the appropriate
  extension (<literal role="extension">tar.gz</literal>, <literal
  role="extension">zip</literal>, <literal role="extension">7z</literal>,
  &etc;) or select a supported format in the <guilabel>Filter</guilabel> combo box
--and check the <guilabel>Automatically select filename extension</guilabel> option.</para>
++and check the <guilabel>Automatically add <replaceable>filename extension</replaceable></guilabel> option.</para>
  
  <para>To add files or folders to the new archive, choose <guimenuitem>Add
  File...</guimenuitem> or <guimenuitem>Add
@@@ -278,6 -277,6 +291,17 @@@ from e.g. &dolphin; into the main &ark
  be added to the current archive. Note that files added in this way will always be added
  to the root directory of the archive.</para>
  
++<para>Additional options are presented in collapsible groups at the bottom of the dialog.
++</para>
++
++<sect2 id="ark-compression">
++<title>Compression</title>
++<para>A higher value generates smaller archives, but results in longer compression and decompression times.
++The default compression level proposed by &ark; is usually a good compromise between size and (de)compression speed.
++For most formats the minimum compression level is equivalent to just storing the files, &ie; applying no compression.
++</para>
++</sect2>
++
  <sect2 id="ark-password-protection">
  <title>Password Protection</title>
  <para>If you create a <literal role="extension">zip</literal>, <literal
@@@ -298,8 -297,8 +322,28 @@@ This is called header encryption and i
  <literal role="extension">rar</literal> and <literal role="extension">7zip</literal>
  formats. Header encryption is enabled by default (when available), in order to offer
  the maximum protection for novice users.</para>
++</sect2>
  
++<sect2 id="ark-multi-volume">
++<title>Multi-volume Archive</title>
++<para>With the <literal role="extension">zip</literal>, <literal role="extension">rar</literal>
++and <literal role="extension">7z</literal> formats you can create multi-volume archives, also
++known as multi-part or split archives.</para>
++<para>A multi-volume archive is one big compressed archive split into several files. This feature
++is useful if the maximum file size is limited, ⪚ by the capacity of a storage medium
++or the maximal size of an email with attachments.</para>
++<para>To create a multi-volume archive, check the <guilabel>Create multi-volume archive</guilabel>
++checkbox and set a maximum <guilabel>Volume size</guilabel> in the dialog. Then add all files to
++the archive and &ark; will automatically generate the required number of archive volumes.
++Depending on the selected format the files have an extension with consecutively numbering
++scheme ⪚ <filename>xxx.7z.001</filename>, <filename>xxx.7z.002</filename> or
++<filename>xxx.zip.001</filename>, <filename>xxx.zip.002</filename> or <filename>xxx.part1.rar</filename>,
++<filename>xxx.part2.rar</filename> &etc;.</para>
++<para>To extract a multi-volume archive, put all archive files into one folder and open the file
++with the lowest extension number in &ark; and all other parts of the split archive
++will be opened automatically.</para>
  </sect2>
++
  </sect1>
  
  </chapter>
diff --cc doc/man-ark.1.docbook
index 9786bca,9786bca..b98cb54
--- a/doc/man-ark.1.docbook
+++ b/doc/man-ark.1.docbook
@@@ -19,8 -19,8 +19,8 @@@
  <contrib>Update of &ark; man page in 2015 and 2016.</contrib>
  <email>rthomsen6 at gmail.com</email></author>
  
--<date>2016-03-19</date><!--Update only when changing/reviewing this man page-->
--<releaseinfo>16.04</releaseinfo><!--Update only when changing/reviewing this man page-->
++<date>2016-08-09</date><!--Update only when changing/reviewing this man page-->
++<releaseinfo>16.08</releaseinfo><!--Update only when changing/reviewing this man page-->
  <productname>KDE Applications</productname>
  </refentryinfo>
  
@@@ -50,7 -50,7 +50,7 @@@ file</replaceable></group
  <group choice="opt"><option>-d</option></group>
  <group choice="opt"><option>-o</option> <replaceable>
  directory</replaceable></group>
--<arg choice="opt">&kde; Generic Options</arg>
++<arg choice="opt">&kf5; Generic Options</arg>
  <arg choice="opt">&Qt; Generic Options</arg>
  </cmdsynopsis>
  </refsynopsisdiv>
@@@ -175,6 -175,6 +175,19 @@@ a subfolder by the name of the archive 
  </refsect1>
  
  <refsect1>
++<title>See Also</title>
++<simplelist>
++<member>More detailed user documentation is available from <ulink
++url="help:/ark">help:/ark</ulink>
++(either enter this &URL; into &konqueror;, or run
++<userinput><command>khelpcenter</command>
++<parameter>help:/ark</parameter></userinput>).</member>
++<member>kf5options(7)</member>
++<member>qt5options(7)</member>
++</simplelist>
++</refsect1>
++
++<refsect1>
  <title>Examples</title>
  
  <variablelist>
diff --cc kerfuffle/addtoarchive.cpp
index 3df09af,cd61f82..c8f8d50
--- a/kerfuffle/addtoarchive.cpp
+++ b/kerfuffle/addtoarchive.cpp
@@@ -113,12 -113,10 +113,12 @@@ bool AddToArchive::showAddDialog(
  
  bool AddToArchive::addInput(const QUrl &url)
  {
 -    m_inputs << url.toLocalFile();
 +    Archive::Entry *entry = new Archive::Entry();
-     entry->setFullPath(url.toDisplayString(QUrl::PreferLocalFile));
++    entry->setFullPath(url.toLocalFile());
 +    m_entries << entry;
  
      if (m_firstPath.isEmpty()) {
-         QString firstEntry = url.toDisplayString(QUrl::PreferLocalFile);
+         QString firstEntry = url.toLocalFile();
          m_firstPath = QFileInfo(firstEntry).dir().absolutePath();
      }
  
diff --cc kerfuffle/archive_kerfuffle.cpp
index 8e6cd59,86cf419..41db04b
--- a/kerfuffle/archive_kerfuffle.cpp
+++ b/kerfuffle/archive_kerfuffle.cpp
@@@ -33,7 -32,13 +33,8 @@@
  #include "mimetypes.h"
  #include "pluginmanager.h"
  
 -#include <QByteArray>
 -#include <QDebug>
  #include <QEventLoop>
 -#include <QFile>
 -#include <QFileInfo>
 -#include <QMimeDatabase>
+ #include <QRegularExpression>
  
  #include <KPluginFactory>
  #include <KPluginLoader>
@@@ -256,11 -322,9 +307,9 @@@ QString Archive::subfolderName(
      return m_subfolderName;
  }
  
 -void Archive::onNewEntry(const ArchiveEntry &entry)
 +void Archive::onNewEntry(const Archive::Entry *entry)
  {
-     if (!entry->isDir()) {
-         m_numberOfFiles++;
-     }
 -    entry[IsDirectory].toBool() ? m_numberOfFolders++ : m_numberOfFiles++;
++    entry->isDir() ? m_numberOfFolders++ : m_numberOfFiles++;
  }
  
  bool Archive::isValid() const
@@@ -307,7 -375,7 +360,7 @@@ DeleteJob* Archive::deleteFiles(QList<A
          return Q_NULLPTR;
      }
  
-     qCDebug(ARK) << "Going to delete entries" << entries;
 -    qCDebug(ARK) << "Going to delete" << files.size() << "files";
++    qCDebug(ARK) << "Going to delete" << entries.size() << "entries";
  
      if (m_iface->isReadOnly()) {
          return 0;
diff --cc kerfuffle/cliinterface.cpp
index 43fc155,b2cc09b..4793a54
--- a/kerfuffle/cliinterface.cpp
+++ b/kerfuffle/cliinterface.cpp
@@@ -231,50 -183,23 +232,54 @@@ bool CliInterface::addFiles(const QList
      }
  
      int compLevel = options.value(QStringLiteral("CompressionLevel"), -1).toInt();
+     ulong volumeSize = options.value(QStringLiteral("VolumeSize"), 0).toULongLong();
  
      const auto args = substituteAddVariables(m_param.value(AddArgs).toStringList(),
 -                                             files,
 +                                             filesToPass,
                                               password(),
                                               isHeaderEncryptionEnabled(),
-                                              compLevel);
+                                              compLevel,
+                                              volumeSize);
  
 -    if (!runProcess(m_param.value(AddProgram).toStringList(), args)) {
 -        return false;
 -    }
 +    return runProcess(m_param.value(AddProgram).toStringList(), args);
 +}
  
 -    return true;
 +bool CliInterface::moveFiles(const QList<Archive::Entry*> &files, Archive::Entry *destination, const CompressionOptions &options)
 +{
++    Q_UNUSED(options);
++
 +    cacheParameterList();
 +    m_operationMode = Move;
 +
 +    m_removedFiles = files;
 +    QList<Archive::Entry*> withoutChildren = entriesWithoutChildren(files);
 +    setNewMovedFiles(files, destination, withoutChildren.count());
 +
 +    const auto moveArgs = m_param.value(MoveArgs).toStringList();
 +
 +    const auto args = substituteMoveVariables(moveArgs, withoutChildren, destination, password());
 +
 +    return runProcess(m_param.value(MoveProgram).toStringList(), args);
 +}
 +
 +bool CliInterface::copyFiles(const QList<Archive::Entry*> &files, Archive::Entry *destination, const CompressionOptions &options)
 +{
 +    m_oldWorkingDir = QDir::currentPath();
 +    m_tempExtractDir = new QTemporaryDir();
 +    m_tempAddDir = new QTemporaryDir();
 +    QDir::setCurrent(m_tempExtractDir->path());
 +    m_passedFiles = files;
 +    m_passedDestination = destination;
 +    m_passedOptions = options;
 +    m_passedOptions[QStringLiteral("PreservePaths")] = true;
 +
 +    m_subOperation = Extract;
 +    connect(this, &CliInterface::finished, this, &CliInterface::continueCopying);
 +
 +    return extractFiles(files, QDir::currentPath(), m_passedOptions);
  }
  
 -bool CliInterface::deleteFiles(const QList<QVariant> & files)
 +bool CliInterface::deleteFiles(const QList<Archive::Entry*> &files)
  {
      cacheParameterList();
      m_operationMode = Delete;
@@@ -296,9 -225,13 +301,9 @@@ bool CliInterface::testArchive(
      cacheParameterList();
      m_operationMode = Test;
  
-     const auto args = substituteTestVariables(m_param.value(TestArgs).toStringList());
+     const auto args = substituteTestVariables(m_param.value(TestArgs).toStringList(), password());
  
 -    if (!runProcess(m_param.value(TestProgram).toStringList(), args)) {
 -        return false;
 -    }
 -
 -    return true;
 +    return runProcess(m_param.value(TestProgram).toStringList(), args);
  }
  
  bool CliInterface::runProcess(const QStringList& programNames, const QStringList& arguments)
@@@ -333,17 -265,15 +337,15 @@@
      m_process->setNextOpenMode(QIODevice::ReadWrite | QIODevice::Unbuffered | QIODevice::Text);
      m_process->setProgram(programPath, arguments);
  
-     connect(m_process, SIGNAL(readyReadStandardOutput()), SLOT(readStdout()), Qt::DirectConnection);
+     connect(m_process, &QProcess::readyReadStandardOutput, this, [=]() {
+         readStdout();
+     });
  
 -    if (m_operationMode == Copy) {
 +    if (m_operationMode == Extract) {
          // Extraction jobs need a dedicated post-processing function.
-         connect(m_process,
-                 static_cast<void (KPtyProcess::*)(int, QProcess::ExitStatus)>(&KPtyProcess::finished),
-                 this,
-                 &CliInterface::extractProcessFinished,
-                 Qt::DirectConnection);
 -        connect(m_process, static_cast<void (KPtyProcess::*)(int, QProcess::ExitStatus)>(&KPtyProcess::finished), this, &CliInterface::copyProcessFinished);
++        connect(m_process, static_cast<void (KPtyProcess::*)(int, QProcess::ExitStatus)>(&KPtyProcess::finished), this, &CliInterface::extractProcessFinished);
      } else {
-         connect(m_process, static_cast<void (KPtyProcess::*)(int, QProcess::ExitStatus)>(&KPtyProcess::finished), this, &CliInterface::processFinished, Qt::DirectConnection);
+         connect(m_process, static_cast<void (KPtyProcess::*)(int, QProcess::ExitStatus)>(&KPtyProcess::finished), this, &CliInterface::processFinished);
      }
  
      m_stdOutData.clear();
@@@ -372,22 -302,13 +374,22 @@@ void CliInterface::processFinished(int 
          return;
      }
  
 -    if (m_operationMode == Delete) {
 -        foreach(const QVariant& v, m_removedFiles) {
 -            emit entryRemoved(v.toString());
 +    if (m_operationMode == Delete || m_operationMode == Move) {
 +        QStringList removedFullPaths = entryFullPaths(m_removedFiles);
 +        foreach (const QString &fullPath, removedFullPaths) {
 +            emit entryRemoved(fullPath);
 +        }
 +        foreach (Archive::Entry *e, m_newMovedFiles) {
 +            emit entry(e);
          }
 +        m_newMovedFiles.clear();
      }
  
-     if (m_operationMode == Add) {
+     if (m_operationMode == Add && !isMultiVolume()) {
 +        if (m_extractTempDir) {
 +            delete m_extractTempDir;
 +            m_extractTempDir = Q_NULLPTR;
 +        }
          list();
      } else if (m_operationMode == List && isCorrupt()) {
          Kerfuffle::LoadCorruptQuery query(filename());
@@@ -752,7 -640,7 +754,7 @@@ QStringList CliInterface::substituteExt
      return args;
  }
  
- QStringList CliInterface::substituteAddVariables(const QStringList &addArgs, const QList<Archive::Entry*> &entries, const QString &password, bool encryptHeader, int compLevel)
 -QStringList CliInterface::substituteAddVariables(const QStringList &addArgs, const QStringList &files, const QString &password, bool encryptHeader, int compLevel, ulong volumeSize)
++QStringList CliInterface::substituteAddVariables(const QStringList &addArgs, const QList<Archive::Entry*> &entries, const QString &password, bool encryptHeader, int compLevel, ulong volumeSize)
  {
      // Required if we call this function from unit tests.
      cacheParameterList();
@@@ -776,8 -664,13 +778,13 @@@
              continue;
          }
  
+         if (arg == QLatin1String("$MultiVolumeSwitch")) {
+             args << multiVolumeSwitch(volumeSize);
+             continue;
+         }
+ 
          if (arg == QLatin1String("$Files")) {
 -            args << files;
 +            args << entryFullPaths(entries, true);
              continue;
          }
  
@@@ -1025,11 -842,29 +1037,29 @@@ QString CliInterface::compressionLevelS
      return compLevelSwitch;
  }
  
+ QString CliInterface::multiVolumeSwitch(ulong volumeSize) const
+ {
+     // The maximum value we allow in the QDoubleSpinBox is 1000MB. Converted to
+     // KB this is 1024000.
+     if (volumeSize <= 0 || volumeSize > 1024000) {
+         return QString();
+     }
+ 
+     Q_ASSERT(m_param.contains(MultiVolumeSwitch));
+ 
+     QString multiVolumeSwitch = m_param.value(MultiVolumeSwitch).toString();
+     Q_ASSERT(!multiVolumeSwitch.isEmpty());
+ 
+     multiVolumeSwitch.replace(QLatin1String("$VolumeSize"), QString::number(volumeSize));
+ 
+     return multiVolumeSwitch;
+ }
+ 
 -QStringList CliInterface::copyFilesList(const QVariantList& files) const
 +QStringList CliInterface::extractFilesList(const QList<Archive::Entry*> &entries) const
  {
      QStringList filesList;
 -    foreach (const QVariant& f, files) {
 -        filesList << escapeFileName(f.value<fileRootNodePair>().file);
 +    foreach (const Archive::Entry *e, entries) {
 +        filesList << escapeFileName(e->fullPath(true));
      }
  
      return filesList;
@@@ -1161,21 -988,7 +1194,21 @@@ void CliInterface::readStdout(bool hand
      }
  }
  
 +bool CliInterface::setAddedFiles()
 +{
 +    QDir::setCurrent(m_tempAddDir->path());
 +    foreach (const Archive::Entry *file, m_passedFiles) {
 +        const QString oldPath = m_tempExtractDir->path() + QLatin1Char('/') + file->fullPath(true);
 +        const QString newPath = m_tempAddDir->path() + QLatin1Char('/') + file->name();
 +        if (!QFile::rename(oldPath, newPath)) {
 +            return false;
 +        }
 +        m_tempAddedFiles << new Archive::Entry(Q_NULLPTR, file->name());
 +    }
 +    return true;
 +}
 +
- void CliInterface::handleLine(const QString& line)
+ bool CliInterface::handleLine(const QString& line)
  {
      // TODO: This should be implemented by each plugin; the way progress is
      //       shown by each CLI application is subject to a lot of variation.
diff --cc kerfuffle/cliinterface.h
index 072a278,af33005..c4ad91e
--- a/kerfuffle/cliinterface.h
+++ b/kerfuffle/cliinterface.h
@@@ -332,19 -311,13 +334,19 @@@ public
      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);
 -    QStringList substituteAddVariables(const QStringList &addArgs, const QStringList &files, const QString &password, bool encryptHeader, int compLevel, ulong volumeSize);
 -    QStringList substituteDeleteVariables(const QStringList &deleteArgs, const QVariantList &files, const QString &password);
 +    QStringList substituteExtractVariables(const QStringList &extractArgs, const QList<Archive::Entry*> &entries, bool preservePaths, const QString &password);
-     QStringList substituteAddVariables(const QStringList &addArgs, const QList<Archive::Entry*> &entries, const QString &password, bool encryptHeader, int compLevel);
++    QStringList substituteAddVariables(const QStringList &addArgs, const QList<Archive::Entry*> &entries, const QString &password, bool encryptHeader, int compLevel, ulong volumeSize);
 +    QStringList substituteMoveVariables(const QStringList &moveArgs, const QList<Archive::Entry*> &entriesWithoutChildren, const Archive::Entry *destination, const QString &password);
 +    QStringList substituteDeleteVariables(const QStringList &deleteArgs, const QList<Archive::Entry*> &entries, const QString &password);
      QStringList substituteCommentVariables(const QStringList &commentArgs, const QString &commentFile);
-     QStringList substituteTestVariables(const QStringList &testArgs);
+     QStringList substituteTestVariables(const QStringList &testArgs, const QString &password);
  
      /**
 +     * @see ArchiveModel::entryPathsFromDestination
 +     */
 +    void setNewMovedFiles(const QList<Archive::Entry*> &entries, const Archive::Entry *destination, int entriesWithoutChildren);
 +
 +    /**
       * @return The preserve path switch, according to the @p preservePaths extraction option.
       */
      QString preservePathSwitch(bool preservePaths) const;
@@@ -367,12 -342,19 +371,36 @@@
      /**
       * @return The list of selected files to extract.
       */
 -    QStringList copyFilesList(const QVariantList& files) const;
 +    QStringList extractFilesList(const QList<Archive::Entry*> &files) const;
  
+     QString multiVolumeName() const Q_DECL_OVERRIDE;
+ 
  protected:
  
 +    bool setAddedFiles();
-     virtual void handleLine(const QString& line);
++
+     /**
+      * Handles the given @p line.
+      * @return True if the line is ok. False if the line contains/triggers a "fatal" error
+      * or a canceled user query. If false is returned, the caller is supposed to call killProcess().
+      */
+     virtual bool handleLine(const QString& line);
+ 
++    bool checkForErrorMessage(const QString& line, int parameterIndex);
++
++    /**
++     * 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);
++
      virtual void cacheParameterList();
  
      /**
@@@ -397,19 -379,33 +425,27 @@@
       */
      bool passwordQuery();
  
 -    /**
 -     * 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 checkForErrorMessage(const QString& line, int parameterIndex);
 +    void cleanUp();
  
 +    QString m_oldWorkingDir;
-     ParameterList m_param;
-     int m_exitCode;
 +    QTemporaryDir *m_tempExtractDir;
 +    QTemporaryDir *m_tempAddDir;
 +    OperationMode m_subOperation;
 +    QList<Archive::Entry*> m_passedFiles;
 +    QList<Archive::Entry*> m_tempAddedFiles;
 +    Archive::Entry *m_passedDestination;
 +    CompressionOptions m_passedOptions;
  
+     ParameterList m_param;
+ 
+ #ifdef Q_OS_WIN
+     KProcess *m_process;
+ #else
+     KPtyProcess *m_process;
+ #endif
+ 
+     bool m_abortingOperation;
+ 
 -
  protected slots:
      virtual void readStdout(bool handleAll = false);
  
@@@ -474,16 -443,9 +495,10 @@@ private
      QRegularExpression m_passwordPromptPattern;
      QHash<int, QList<QRegularExpression> > m_patternCache;
  
- #ifdef Q_OS_WIN
-     KProcess *m_process;
- #else
-     KPtyProcess *m_process;
- #endif
- 
 -    QVariantList m_removedFiles;
 +    QList<Archive::Entry*> m_removedFiles;
 +    QList<Archive::Entry*> m_newMovedFiles;
+     int m_exitCode;
      bool m_listEmptyLines;
-     bool m_abortingOperation;
      QString m_storedFileName;
  
      CompressionOptions m_compressionOptions;
diff --cc kerfuffle/jobs.cpp
index bb815b1,bedc3bf..8b9aa02
--- a/kerfuffle/jobs.cpp
+++ b/kerfuffle/jobs.cpp
@@@ -31,6 -29,6 +31,7 @@@
  #include "ark_debug.h"
  
  #include <QDir>
++#include <QDirIterator>
  #include <QFileInfo>
  #include <QRegularExpression>
  #include <QThread>
@@@ -305,8 -300,7 +306,8 @@@ void ExtractJob::doWork(
  
      connectToArchiveInterfaceSignals();
  
-     qCDebug(ARK) << "Starting extraction with selected files:"
 -    qCDebug(ARK) << "Starting extraction with" << m_files.count() << "selected files."
++    qCDebug(ARK) << "Starting extraction with" << m_entries.count() << "selected files."
 +             << m_entries
               << "Destination dir:" << m_destinationDir
               << "Options:" << m_options;
  
@@@ -341,17 -335,17 +342,17 @@@ ExtractionOptions ExtractJob::extractio
      return m_options;
  }
  
 -TempExtractJob::TempExtractJob(const QString &file, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface)
 +TempExtractJob::TempExtractJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface)
      : Job(interface)
 -    , m_file(file)
 +    , m_entry(entry)
      , m_passwordProtectedHint(passwordProtectedHint)
  {
+     m_tmpExtractDir = new QTemporaryDir();
  }
  
- 
  QString TempExtractJob::validatedFilePath() const
  {
 -    QString path = extractionDir() + QLatin1Char('/') + m_file;
 +    QString path = extractionDir() + QLatin1Char('/') + m_entry->fullPath();
  
      // Make sure a maliciously crafted archive with parent folders named ".." do
      // not cause the previewed file path to be located outside the temporary
@@@ -388,37 -387,25 +394,25 @@@ void TempExtractJob::doWork(
      }
  }
  
- PreviewJob::PreviewJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface)
-     : TempExtractJob(entry, passwordProtectedHint, interface)
+ QString TempExtractJob::extractionDir() const
  {
-     qCDebug(ARK) << "PreviewJob started";
+     return m_tmpExtractDir->path();
  }
  
- QString PreviewJob::extractionDir() const
 -PreviewJob::PreviewJob(const QString& file, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface)
 -    : TempExtractJob(file, passwordProtectedHint, interface)
++PreviewJob::PreviewJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface)
++    : TempExtractJob(entry, passwordProtectedHint, interface)
  {
-     return m_tmpExtractDir.path();
+     qCDebug(ARK) << "PreviewJob started";
  }
  
 -OpenJob::OpenJob(const QString& file, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface)
 -    : TempExtractJob(file, passwordProtectedHint, interface)
 +OpenJob::OpenJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface)
 +    : TempExtractJob(entry, passwordProtectedHint, interface)
  {
      qCDebug(ARK) << "OpenJob started";
- 
-     m_tmpExtractDir = new QTemporaryDir();
- }
- 
- QTemporaryDir *OpenJob::tempDir() const
- {
-     return m_tmpExtractDir;
- }
- 
- QString OpenJob::extractionDir() const
- {
-     return m_tmpExtractDir->path();
  }
  
 -OpenWithJob::OpenWithJob(const QString& file, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface)
 -    : OpenJob(file, passwordProtectedHint, interface)
 +OpenWithJob::OpenWithJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface)
 +    : OpenJob(entry, passwordProtectedHint, interface)
  {
      qCDebug(ARK) << "OpenWithJob started";
  }
@@@ -434,15 -420,15 +428,7 @@@ AddJob::AddJob(const QList<Archive::Ent
  
  void AddJob::doWork()
  {
-     qCDebug(ARK) << "AddJob: going to add" << m_entries.count() << "file(s)";
 -    qCDebug(ARK) << "AddJob: going to add" << m_files.count() << "file(s)";
--
-     emit description(this, i18np("Adding a file", "Adding %1 files", m_entries.count()));
 -    emit description(this, i18np("Adding a file", "Adding %1 files", m_files.count()));
--
--    ReadWriteArchiveInterface *m_writeInterface =
--        qobject_cast<ReadWriteArchiveInterface*>(archiveInterface());
--
--    Q_ASSERT(m_writeInterface);
--
++    // Set current dir.
      const QString globalWorkDir = m_options.value(QStringLiteral("GlobalWorkDir")).toString();
      const QDir workDir = globalWorkDir.isEmpty() ? QDir::current() : QDir(globalWorkDir);
      if (!globalWorkDir.isEmpty()) {
@@@ -451,14 -437,14 +437,41 @@@
          QDir::setCurrent(globalWorkDir);
      }
  
++    // Count total number of entries to be added.
++    qulonglong totalCount = 0;
++    QElapsedTimer timer;
++    timer.start();
++    foreach (const Archive::Entry* entry, m_entries) {
++        totalCount++;
++        if (QFileInfo(entry->fullPath()).isDir()) {
++            QDirIterator it(entry->fullPath(), QDir::AllEntries | QDir::Readable | QDir::Hidden |
++                            QDir::NoDotAndDotDot, QDirIterator::Subdirectories);
++            while (it.hasNext()) {
++                it.next();
++                totalCount++;
++            }
++        }
++    }
++    qCDebug(ARK) << "Counted" << totalCount << "entries in" << timer.elapsed() << "ms";
++
++    m_options[QStringLiteral("NumberOfEntries")] = totalCount;
++
++    qCDebug(ARK) << "AddJob: going to add" << totalCount << "entries";
++    emit description(this, i18np("Adding a file", "Adding %1 files", totalCount));
++
++    ReadWriteArchiveInterface *m_writeInterface =
++        qobject_cast<ReadWriteArchiveInterface*>(archiveInterface());
++
++    Q_ASSERT(m_writeInterface);
++
      // The file paths must be relative to GlobalWorkDir.
 -    QStringList relativeFiles;
 -    foreach (const QString& file, m_files) {
 +    foreach (Archive::Entry *entry, m_entries) {
          // #191821: workDir must be used instead of QDir::current()
          //          so that symlinks aren't resolved automatically
 -        QString relativePath = workDir.relativeFilePath(file);
 +        const QString &fullPath = entry->fullPath();
 +        QString relativePath = workDir.relativeFilePath(fullPath);
  
 -        if (file.endsWith(QLatin1Char('/'))) {
 +        if (fullPath.endsWith(QLatin1Char('/'))) {
              relativePath += QLatin1Char('/');
          }
  
diff --cc kerfuffle/jobs.h
index 50f3c28,0fdd7be..196b0bf
--- a/kerfuffle/jobs.h
+++ b/kerfuffle/jobs.h
@@@ -166,9 -169,10 +172,10 @@@ public slots
      virtual void doWork() Q_DECL_OVERRIDE;
  
  private:
-     virtual QString extractionDir() const = 0;
+     QString extractionDir() const;
  
 -    QString m_file;
 +    Archive::Entry *m_entry;
+     QTemporaryDir *m_tmpExtractDir;
      bool m_passwordProtectedHint;
  };
  
@@@ -181,12 -185,7 +188,7 @@@ class KERFUFFLE_EXPORT PreviewJob : pub
      Q_OBJECT
  
  public:
 -    PreviewJob(const QString& file, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface);
 +    PreviewJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface);
- 
- private:
-     QString extractionDir() const Q_DECL_OVERRIDE;
- 
-     QTemporaryDir m_tmpExtractDir;
  };
  
  /**
@@@ -198,18 -197,7 +200,7 @@@ class KERFUFFLE_EXPORT OpenJob : publi
      Q_OBJECT
  
  public:
 -    OpenJob(const QString& file, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface);
 +    OpenJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface);
- 
-     /**
-      * @return The temporary dir used for the extraction.
-      * It is safe to delete this pointer in order to remove the directory.
-      */
-     QTemporaryDir *tempDir() const;
- 
- private:
-     QString extractionDir() const Q_DECL_OVERRIDE;
- 
-     QTemporaryDir *m_tmpExtractDir;
  };
  
  class KERFUFFLE_EXPORT OpenWithJob : public OpenJob
diff --cc kerfuffle/mime/kerfuffle.xml
index 00b1101,c7b2824..c02d807
--- a/kerfuffle/mime/kerfuffle.xml
+++ b/kerfuffle/mime/kerfuffle.xml
@@@ -15,7 -48,51 +48,56 @@@
     </mime-type>
     <mime-type type="application/x-lz4-compressed-tar">
        <comment>Tar archive (LZ4-compressed)</comment>
-       <comment xml:lang="en">Tar archive (LZ4-compressed)</comment>
+       <comment xml:lang="ca">Arxiu tar (comprimit amb LZ4)</comment>
+       <comment xml:lang="ca at valencia">Arxiu tar (comprimit amb LZ4)</comment>
+       <comment xml:lang="cs">Archiv Tar (komprimovaný LZ4)</comment>
+       <comment xml:lang="de">Tar-Archiv (LZ4-komprimiert)</comment>
+       <comment xml:lang="en_GB">Tar archive (LZ4-compressed)</comment>
+       <comment xml:lang="es">Archivo comprimido Tar (comprimido con LZ4)</comment>
+       <comment xml:lang="nl">Tar-archief (lz4-gecomprimeerd)</comment>
+       <comment xml:lang="pl">Archiwum Tar (kompresja LZ4)</comment>
+       <comment xml:lang="pt">Pacote TAR (comprimido com LZ4)</comment>
+       <comment xml:lang="sk">Tar archív (LZ4 komprimovaný)</comment>
+       <comment xml:lang="sl">Arhiv tar (stisnjen z LZ4)</comment>
+       <comment xml:lang="sr">тар архива (ЛЗ4‑компресована)</comment>
+       <comment xml:lang="sr at ijekavian">тар архива (ЛЗ4‑компресована)</comment>
+       <comment xml:lang="sr at ijekavianlatin">tar arhiva (LZ4-kompresovana)</comment>
+       <comment xml:lang="sr at latin">tar arhiva (LZ4-kompresovana)</comment>
+       <comment xml:lang="sv">Tar-arkiv (LZ4-komprimerat)</comment>
+       <comment xml:lang="uk">архів Tar (стиснутий LZ4)</comment>
        <glob pattern="*.tar.lz4"/>
     </mime-type>
+    <mime-type type="application/x-iso9660-appimage">
+       <comment>AppImage application bundle</comment>
+       <comment xml:lang="ca">Paquet d'aplicació «AppImage»</comment>
+       <comment xml:lang="ca at valencia">Paquet d'aplicació «AppImage»</comment>
++      <comment xml:lang="de">AppImage-Anwendungspaket</comment>
+       <comment xml:lang="en_GB">AppImage application bundle</comment>
+       <comment xml:lang="es">Paquete de aplicación AppImage</comment>
+       <comment xml:lang="nl">Toepassingsbundel AppImage</comment>
+       <comment xml:lang="pl">Pęk aplikacji AppImage</comment>
+       <comment xml:lang="pt">Pacote de aplicação AppImage</comment>
+       <comment xml:lang="sk">Balík aplikácie AppImage</comment>
+       <comment xml:lang="sl">Zbirka programov AppImage</comment>
++      <comment xml:lang="sr">ап‑имејџ пакет програма</comment>
++      <comment xml:lang="sr at ijekavian">ап‑имејџ пакет програма</comment>
++      <comment xml:lang="sr at ijekavianlatin">AppImage paket programa</comment>
++      <comment xml:lang="sr at latin">AppImage paket programa</comment>
+       <comment xml:lang="sv">AppImage programpacke</comment>
+       <comment xml:lang="uk">пакунок програми AppImage</comment>
+       <sub-class-of type="application/x-executable"/>
+       <sub-class-of type="application/x-cd-image"/>
+       <generic-icon name="application-x-executable"/>
+       <magic priority="50">
+          <match value="ELF" type="string" offset="1" >
+             <match value="0x41" type="byte" offset="8">
+                <match value="0x49" type="byte" offset="9">
+                   <match value="0x01" type="byte" offset="10"/>
+                </match>
+             </match>
+          </match>
+       </magic>
+       <glob pattern="*.AppImage"/>
+    </mime-type>
  </mime-info>
+ 
diff --cc part/archivemodel.cpp
index 85e5389,3587a77..4bdc6d0
--- a/part/archivemodel.cpp
+++ b/part/archivemodel.cpp
@@@ -28,8 -29,13 +28,9 @@@
  #include <KLocalizedString>
  #include <kio/global.h>
  
 -#include <QDateTime>
  #include <QDBusConnection>
+ #include <QElapsedTimer>
  #include <QMimeData>
 -#include <QMimeDatabase>
 -#include <QPersistentModelIndex>
  #include <QRegularExpression>
  #include <QUrl>
  
@@@ -147,12 -272,21 +148,14 @@@ private
      Qt::SortOrder m_sortOrder;
  };
  
 -int ArchiveNode::row() const
 -{
 -    if (parent()) {
 -        return parent()->entries().indexOf(const_cast<ArchiveNode*>(this));
 -    }
 -    return 0;
 -}
 -
  ArchiveModel::ArchiveModel(const QString &dbusPathName, QObject *parent)
      : QAbstractItemModel(parent)
 -    , m_rootNode(new ArchiveDirNode(0, ArchiveEntry()))
 +    , m_rootEntry()
      , m_dbusPathName(dbusPathName)
+     , m_numberOfFiles(0)
+     , m_numberOfFolders(0)
  {
 +    m_rootEntry.setProperty("isDirectory", true);
  }
  
  ArchiveModel::~ArchiveModel()
@@@ -672,16 -843,20 +683,20 @@@ void ArchiveModel::newEntry(Archive::En
  
  void ArchiveModel::slotLoadingFinished(KJob *job)
  {
-     int i = 0;
-     foreach(Archive::Entry *entry, m_newArchiveEntries) {
-         newEntry(entry, DoNotNotifyViews);
-         i++;
-     }
-     beginResetModel();
-     endResetModel();
-     m_newArchiveEntries.clear();
+     if (!job->error()) {
+         QElapsedTimer timer;
+         timer.start();
+         int i = 0;
 -        foreach(const ArchiveEntry &entry, m_newArchiveEntries) {
++        foreach(Archive::Entry *entry, m_newArchiveEntries) {
+             newEntry(entry, DoNotNotifyViews);
+             i++;
+         }
+         beginResetModel();
+         endResetModel();
+         m_newArchiveEntries.clear();
  
-     qCDebug(ARK) << "Added" << i << "entries to model";
+         qCDebug(ARK) << "Added" << i << "entries to model in" << timer.elapsed() << "ms";
+     }
  
      emit loadingFinished(job);
  }
@@@ -985,3 -1023,52 +1000,52 @@@ void ArchiveModel::slotCleanupEmptyDirs
          endRemoveRows();
      }
  }
+ 
+ void ArchiveModel::countEntriesAndSize() {
+ 
+     // This function is used to count the number of folders/files and
+     // the total compressed size. This is needed for PropertiesDialog
+     // to update the corresponding values after adding/deleting files.
+ 
+     // When ArchiveModel has been properly fixed, this code can likely
+     // be removed.
+ 
+     m_numberOfFiles = 0;
+     m_numberOfFolders = 0;
+     m_uncompressedSize = 0;
+ 
+     QElapsedTimer timer;
+     timer.start();
+ 
 -    traverseAndCountDirNode(m_rootNode);
++    traverseAndCountDirNode(&m_rootEntry);
+ 
+     qCDebug(ARK) << "Time to count entries and size:" << timer.elapsed() << "ms";
+ }
+ 
 -void ArchiveModel::traverseAndCountDirNode(ArchiveDirNode *dir)
++void ArchiveModel::traverseAndCountDirNode(Archive::Entry *dir)
+ {
 -    foreach(ArchiveNode *node, dir->entries()) {
 -        if (node->isDir()) {
 -            traverseAndCountDirNode(dynamic_cast<ArchiveDirNode*>(node));
++    foreach(Archive::Entry *entry, dir->entries()) {
++        if (entry->isDir()) {
++            traverseAndCountDirNode(entry);
+             m_numberOfFolders++;
+         } else {
+             m_numberOfFiles++;
 -            m_uncompressedSize += node->entry()[Size].toULongLong();
++            m_uncompressedSize += entry->property("size").toULongLong();
+         }
+     }
+ }
+ 
+ qulonglong ArchiveModel::numberOfFiles() const
+ {
+     return m_numberOfFiles;
+ }
+ 
+ qulonglong ArchiveModel::numberOfFolders() const
+ {
+     return m_numberOfFolders;
+ }
+ 
+ qulonglong ArchiveModel::uncompressedSize() const
+ {
+     return m_uncompressedSize;
+ }
diff --cc part/archivemodel.h
index fcca1ca,5baffaf..0290d8f
--- a/part/archivemodel.h
+++ b/part/archivemodel.h
@@@ -26,10 -25,11 +26,11 @@@
  #include <QAbstractItemModel>
  #include <QScopedPointer>
  
+ #include <KMessageWidget>
  #include <kjobtrackerinterface.h>
 -#include "kerfuffle/archive_kerfuffle.h"
 +#include "kerfuffle/archiveentry.h"
  
 -using Kerfuffle::ArchiveEntry;
 +using Kerfuffle::Archive;
  
  namespace Kerfuffle
  {
@@@ -85,41 -86,22 +86,47 @@@ public
       */
      void encryptArchive(const QString &password, bool encryptHeader);
  
+     void countEntriesAndSize();
+     qulonglong numberOfFiles() const;
+     qulonglong numberOfFolders() const;
+     qulonglong uncompressedSize() const;
+ 
 +    /**
 +     * Constructs a list of conflicting entries.
 +     *
 +     * @param conflictingEntries Reference to the empty mutable entries list, which will be constructed.
 +     * If the method returns false, this list will contain only entries which produce a critical conflict.
 +     * @param entries New entries paths list.
 +     * @param allowMerging Boolean variable indicating whether merging is permitted.
 +     * If true, existing entries won't generate an error.
 +     *
 +     * @return Boolean variable indicating whether conflicts are not critical (true for not critical,
 +     * false for critical). For example, if there are both "some/file" (not a directory) and "some/file/" (a directory)
 +     * entries for both new and existing paths, the method will return false. Also, if merging is not allowed,
 +     * this method will return false for entries with the same path and types.
 +     */
 +    bool conflictingEntries(QList<const Archive::Entry*> &conflictingEntries, const QStringList &entries, bool allowMerging) const;
 +
 +    static bool hasDuplicatedEntries(const QStringList &entries);
 +
 +    static QMap<QString, Archive::Entry*> entryMap(const QList<Archive::Entry*> &entries);
 +
 +    const QHash<QString, QIcon> entryIcons() const;
 +
 +    QMap<QString, Kerfuffle::Archive::Entry*> filesToMove;
 +    QMap<QString, Kerfuffle::Archive::Entry*> filesToCopy;
 +
  signals:
      void loadingStarted();
      void loadingFinished(KJob *);
      void extractionFinished(bool success);
      void error(const QString& error, const QString& details);
 -    void droppedFiles(const QStringList& files, const QString& path = QString());
 +    void droppedFiles(const QStringList& files, const Archive::Entry*, const QString&);
+     void messageWidget(KMessageWidget::MessageType type, const QString& msg);
  
  private slots:
 -    void slotNewEntryFromSetArchive(const ArchiveEntry& entry);
 -    void slotNewEntry(const ArchiveEntry& entry);
 +    void slotNewEntryFromSetArchive(Archive::Entry *entry);
 +    void slotNewEntry(Archive::Entry *entry);
      void slotLoadingFinished(KJob *job);
      void slotEntryRemoved(const QString & path);
      void slotUserQuery(Kerfuffle::Query *query);
@@@ -146,16 -128,21 +153,22 @@@ private
       * of the change.
       */
      enum InsertBehaviour { NotifyViews, DoNotNotifyViews };
 -    void insertNode(ArchiveNode *node, InsertBehaviour behaviour = NotifyViews);
 -    void newEntry(const Kerfuffle::ArchiveEntry& entry, InsertBehaviour behaviour);
 +    void insertEntry(Archive::Entry *entry, InsertBehaviour behaviour = NotifyViews);
 +    void newEntry(Kerfuffle::Archive::Entry *receivedEntry, InsertBehaviour behaviour);
  
 -    void traverseAndCountDirNode(ArchiveDirNode *dir);
++    void traverseAndCountDirNode(Archive::Entry *dir);
+ 
 -    QList<Kerfuffle::ArchiveEntry> m_newArchiveEntries; // holds entries from opening a new archive until it's totally open
 +    QList<Kerfuffle::Archive::Entry*> m_newArchiveEntries; // holds entries from opening a new archive until it's totally open
      QList<int> m_showColumns;
      QScopedPointer<Kerfuffle::Archive> m_archive;
 -    ArchiveDirNode *m_rootNode;
 +    Archive::Entry m_rootEntry;
 +    QHash<QString, QIcon> m_entryIcons;
  
      QString m_dbusPathName;
+ 
+     qulonglong m_numberOfFiles;
+     qulonglong m_numberOfFolders;
+     qulonglong m_uncompressedSize;
  };
  
  #endif // ARCHIVEMODEL_H
diff --cc part/part.cpp
index b6a9f59,3a71679..24ccb9e
--- a/part/part.cpp
+++ b/part/part.cpp
@@@ -173,9 -168,11 +173,11 @@@ Part::Part(QWidget *parentWidget, QObje
      connect(m_model, &ArchiveModel::loadingFinished,
              this, &Part::slotLoadingFinished);
      connect(m_model, &ArchiveModel::droppedFiles,
 -            this, static_cast<void (Part::*)(const QStringList&, const QString&)>(&Part::slotAddFiles));
 +            this, static_cast<void (Part::*)(const QStringList&, const Archive::Entry*, const QString&)>(&Part::slotAddFiles));
      connect(m_model, &ArchiveModel::error,
              this, &Part::slotError);
+     connect(m_model, &ArchiveModel::messageWidget,
+             this, &Part::displayMsgWidget);
  
      connect(this, &Part::busy,
              this, &Part::setBusyGui);
@@@ -368,16 -367,11 +370,17 @@@ void Part::setupActions(
  
      m_addFilesAction = actionCollection()->addAction(QStringLiteral("add"));
      m_addFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-insert")));
 -    m_addFilesAction->setText(i18n("Add &Files..."));
 +    m_addFilesAction->setText(i18n("Add &Files to..."));
      m_addFilesAction->setToolTip(i18nc("@info:tooltip", "Click to add files to the archive"));
+     actionCollection()->setDefaultShortcut(m_addFilesAction, Qt::ALT + Qt::Key_A);
 -    connect(m_addFilesAction, &QAction::triggered,
 -            this, static_cast<void (Part::*)()>(&Part::slotAddFiles));
 +    connect(m_addFilesAction, &QAction::triggered, this, static_cast<void (Part::*)()>(&Part::slotAddFiles));
 +
 +    m_renameFileAction = actionCollection()->addAction(QStringLiteral("rename"));
 +    m_renameFileAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename")));
 +    m_renameFileAction->setText(i18n("&Rename"));
 +    actionCollection()->setDefaultShortcut(m_renameFileAction, Qt::Key_F2);
 +    m_renameFileAction->setToolTip(i18nc("@info:tooltip", "Click to rename the selected file"));
 +    connect(m_renameFileAction, &QAction::triggered, this, &Part::slotEditFileName);
  
      m_deleteFilesAction = actionCollection()->addAction(QStringLiteral("delete"));
      m_deleteFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-remove")));
@@@ -438,9 -413,28 +441,28 @@@
  void Part::updateActions()
  {
      bool isWritable = m_model->archive() && !m_model->archive()->isReadOnly();
 -    bool isDirectory = m_model->entryForIndex(m_view->selectionModel()->currentIndex())[IsDirectory].toBool();
 +    const Archive::Entry *entry = m_model->entryForIndex(m_view->selectionModel()->currentIndex());
      int selectedEntriesCount = m_view->selectionModel()->selectedRows().count();
  
+     // We disable adding files if the archive is encrypted but the password is
+     // unknown (this happens when opening existing non-he password-protected
+     // archives). If we added files they would not get encrypted resulting in an
+     // archive with a mixture of encrypted and unencrypted files.
+     const bool isEncryptedButUnknownPassword = m_model->archive() &&
+                                                m_model->archive()->encryptionType() != Archive::Unencrypted &&
+                                                m_model->archive()->password().isEmpty();
+ 
+     if (isEncryptedButUnknownPassword) {
+         m_addFilesAction->setToolTip(xi18nc("@info:tooltip",
+                                             "Adding files to existing password-protected archives with no header-encryption is currently not supported."
+                                             "<nl/><nl/>Extract the files and create a new archive if you want to add files."));
+         m_testArchiveAction->setToolTip(xi18nc("@info:tooltip",
+                                                "Testing password-protected archives with no header-encryption is currently not supported."));
+     } else {
+         m_addFilesAction->setToolTip(i18nc("@info:tooltip", "Click to add files to the archive"));
+         m_testArchiveAction->setToolTip(i18nc("@info:tooltip", "Click to test the archive for integrity"));
+     }
+ 
      // Figure out if entry size is larger than preview size limit.
      const int maxPreviewSize = ArkSettings::previewFileSizeLimit() * 1024 * 1024;
      const bool limit = ArkSettings::limitPreviewFileSize();
@@@ -1527,20 -1293,18 +1559,31 @@@ void Part::slotAddFilesDone(KJob* job
      } else {
          // Hide the "archive will be created as soon as you add a file" message.
          m_messageWidget->hide();
+ 
+         // For multi-volume archive, we need to re-open the archive after adding files
+         // because the name changes from e.g name.rar to name.part1.rar.
+         if (m_model->archive()->isMultiVolume()) {
+             qCDebug(ARK) << "Multi-volume archive detected, re-opening...";
+             KParts::OpenUrlArguments args = arguments();
+             args.metaData()[QStringLiteral("createNewArchive")] = QStringLiteral("false");
+             setArguments(args);
+ 
+             openUrl(QUrl::fromLocalFile(m_model->archive()->multiVolumeName()));
+         }
      }
 +    m_cutIndexes.clear();
 +    m_model->filesToMove.clear();
 +    m_model->filesToCopy.clear();
 +}
 +
 +void Part::slotPasteFilesDone(KJob *job)
 +{
 +    if (job->error() && job->error() != KJob::KilledJobError) {
 +        KMessageBox::error(widget(), job->errorString());
 +    }
 +    m_cutIndexes.clear();
 +    m_model->filesToMove.clear();
 +    m_model->filesToCopy.clear();
  }
  
  void Part::slotDeleteFilesDone(KJob* job)
@@@ -1653,11 -1418,11 +1700,11 @@@ void Part::slotShowContextMenu(
  
  void Part::displayMsgWidget(KMessageWidget::MessageType type, const QString& msg)
  {
--    // The widget could be already visible, so hide it.
--    m_messageWidget->hide();
--    m_messageWidget->setText(msg);
--    m_messageWidget->setMessageType(type);
--    m_messageWidget->animatedShow();
++    KMessageWidget *msgWidget = new KMessageWidget();
++    msgWidget->setText(msg);
++    msgWidget->setMessageType(type);
++    m_vlayout->insertWidget(0, msgWidget);
++    msgWidget->animatedShow();
  }
  
  } // namespace Ark
diff --cc part/part.h
index ce62810,00b5a54..9fa850a
--- a/part/part.h
+++ b/part/part.h
@@@ -168,14 -139,13 +169,13 @@@ private
      void setupActions();
      bool isSingleFolderArchive() const;
      QString detectSubfolder() const;
 -    QList<QVariant> filesForIndexes(const QModelIndexList& list) const;
 -    QList<QVariant> filesAndRootNodesForIndexes(const QModelIndexList& list) const;
 +    QList<Kerfuffle::Archive::Entry*> filesForIndexes(const QModelIndexList& list) const;
 +    QList<Kerfuffle::Archive::Entry*> filesAndRootNodesForIndexes(const QModelIndexList& list) const;
      QModelIndexList addChildren(const QModelIndexList &list) const;
      void registerJob(KJob *job);
-     void displayMsgWidget(KMessageWidget::MessageType type, const QString& msg);
  
      ArchiveModel         *m_model;
 -    QTreeView            *m_view;
 +    ArchiveView          *m_view;
      QAction *m_previewAction;
      QAction *m_openFileAction;
      QAction *m_openFileWithAction;
@@@ -194,14 -160,10 +194,14 @@@
      KToggleAction *m_showInfoPanelAction;
      InfoPanel            *m_infoPanel;
      QSplitter            *m_splitter;
-     QList<QTemporaryDir*>      m_tmpOpenDirList;
+     QList<QTemporaryDir*>      m_tmpExtractDirList;
      bool                  m_busy;
 +    bool                  m_archiveIsLoaded;
      OpenFileMode m_openFileMode;
      QUrl m_lastUsedAddPath;
 +    QList<Kerfuffle::Archive::Entry*> m_jobTempEntries;
 +    Kerfuffle::Archive::Entry *m_destination;
 +    QModelIndexList m_cutIndexes;
  
      KAbstractWidgetJobTracker  *m_jobTracker;
      KParts::StatusBarExtension *m_statusBarExtension;
diff --cc plugins/cli7zplugin/cliplugin.cpp
index 51837af,4f30e33..ae385ac
--- a/plugins/cli7zplugin/cliplugin.cpp
+++ b/plugins/cli7zplugin/cliplugin.cpp
@@@ -82,11 -80,8 +83,12 @@@ ParameterList CliPlugin::parameterList(
                                     << QStringLiteral("$Archive")
                                     << QStringLiteral("$PasswordSwitch")
                                     << QStringLiteral("$CompressionLevelSwitch")
+                                    << QStringLiteral("$MultiVolumeSwitch")
                                     << QStringLiteral("$Files");
 +        p[MoveArgs] = QStringList() << QStringLiteral("rn")
 +                                    << QStringLiteral("$PasswordSwitch")
 +                                    << QStringLiteral("$Archive")
 +                                    << QStringLiteral("$PathPairs");
          p[DeleteArgs] = QStringList() << QStringLiteral("d")
                                        << QStringLiteral("$PasswordSwitch")
                                        << QStringLiteral("$Archive")
diff --cc plugins/clirarplugin/cliplugin.cpp
index d701a8b,f345211..5e132ca
--- a/plugins/clirarplugin/cliplugin.cpp
+++ b/plugins/clirarplugin/cliplugin.cpp
@@@ -58,8 -57,21 +57,9 @@@ void CliPlugin::resetParsing(
      m_parseState = ParseStateTitle;
      m_remainingIgnoreLines = 1;
      m_comment.clear();
+     m_numberOfVolumes = 0;
  }
  
 -// #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;
@@@ -101,11 -114,8 +102,12 @@@
                                     << QStringLiteral( "$Archive" )
                                     << QStringLiteral("$PasswordSwitch")
                                     << QStringLiteral("$CompressionLevelSwitch")
+                                    << QStringLiteral("$MultiVolumeSwitch")
                                     << QStringLiteral( "$Files" );
 +        p[MoveArgs] = QStringList() << QStringLiteral( "rn" )
 +                                    << QStringLiteral( "$PasswordSwitch" )
 +                                    << QStringLiteral( "$Archive" )
 +                                    << QStringLiteral( "$PathPairs" );
          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" )
diff --cc plugins/cliunarchiverplugin/cliplugin.cpp
index 3fe0c31,cdda7e8..a973419
--- a/plugins/cliunarchiverplugin/cliplugin.cpp
+++ b/plugins/cliunarchiverplugin/cliplugin.cpp
@@@ -21,6 -21,9 +21,7 @@@
   */
  
  #include "cliplugin.h"
 -#include "ark_debug.h"
 -#include "kerfuffle_export.h"
+ #include "queries.h"
  
  #include <QJsonArray>
  #include <QJsonParseError>
@@@ -49,34 -53,10 +51,10 @@@ bool CliPlugin::list(
      m_operationMode = List;
  
      const auto args = substituteListVariables(m_param.value(ListArgs).toStringList(), password());
- 
-     if (!runProcess(m_param.value(ListProgram).toStringList(), args)) {
-         return false;
-     }
- 
-     if (!password().isEmpty()) {
- 
-         // lsar -json exits with error code 1 if the archive is header-encrypted and the password is wrong.
-         if (m_exitCode == 1) {
-             qCWarning(ARK) << "Wrong password, list() aborted";
-             emit error(i18n("Wrong password."));
-             emit finished(false);
-             killProcess();
-             setPassword(QString());
-             return false;
-         }
- 
-         // lsar -json exits with error code 2 if the archive is header-encrypted and no password is given as argument.
-         // At this point we have already asked a password to the user, so we can just list() again.
-         if (m_exitCode == 2) {
-             return CliPlugin::list();
-         }
-     }
- 
-     return true;
+     return runProcess(m_param.value(ListProgram).toStringList(), args);
  }
  
 -bool CliPlugin::copyFiles(const QList<QVariant> &files, const QString &destinationDirectory, const ExtractionOptions &options)
 +bool CliPlugin::extractFiles(const QList<Archive::Entry*> &files, const QString &destinationDirectory, const ExtractionOptions &options)
  {
      ExtractionOptions newOptions = options;
  


More information about the kde-doc-english mailing list