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>[email protected]</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@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@ijekavian">тар архива (ЛЗ4‑компресована)</comment> + <comment xml:lang="sr@ijekavianlatin">tar arhiva (LZ4-kompresovana)</comment> + <comment xml:lang="sr@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@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@ijekavian">ап‑имејџ пакет програма</comment> ++ <comment xml:lang="sr@ijekavianlatin">AppImage paket programa</comment> ++ <comment xml:lang="sr@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;
