Commit bf6425d1 authored by Ragnar Thomsen's avatar Ragnar Thomsen

Add multi-volume support

Support for creating multi-volume rar, 7z and zip archives was
implemented. A QDoubleSpinBox was added to CompressionOptionsWidget
which allows setting the volume size in megabytes between 0.1 to 1000.
The size in megabytes is converted by CompressionOptionsWidget to
kilobytes because 7z doesn't support volume sizes with decimals.

Creating a multi-volume archive changes the archive name
(name.part1.rar, name.7z.001, name.zip.001) so we need to re-open the
archive (the first volume) after adding files.

We only support adding files once, so add/delete actions are disabled in
Part if archive is multi-volume and non-empty.

FEATURE: 124180
FIXED-IN: 16.08.0
Differential Revision: D2194
GUI:
parent eb63d879
......@@ -300,6 +300,10 @@ void MainWindow::newArchive()
if (dialog.data()->compressionLevel() > -1) {
m_openArgs.metaData()[QStringLiteral("compressionLevel")] = QString::number(dialog.data()->compressionLevel());
}
if (dialog.data()->volumeSize() > 0) {
qCDebug(ARK) << "Setting volume size:" << QString::number(dialog.data()->volumeSize());
m_openArgs.metaData()[QStringLiteral("volumeSize")] = QString::number(dialog.data()->volumeSize());
}
m_openArgs.metaData()[QStringLiteral("encryptionPassword")] = password;
if (dialog.data()->isHeaderEncryptionEnabled()) {
......
......@@ -305,7 +305,7 @@ void Cli7zTest::testAddArgs()
QFETCH(bool, encryptHeader);
QFETCH(int, compressionLevel);
QStringList replacedArgs = plugin->substituteAddVariables(addArgs, {}, password, encryptHeader, compressionLevel);
QStringList replacedArgs = plugin->substituteAddVariables(addArgs, {}, password, encryptHeader, compressionLevel, 0);
QFETCH(QStringList, expectedArgs);
QCOMPARE(replacedArgs, expectedArgs);
......
......@@ -334,7 +334,7 @@ void CliRarTest::testAddArgs()
QFETCH(bool, encryptHeader);
QFETCH(int, compressionLevel);
QStringList replacedArgs = rarPlugin->substituteAddVariables(addArgs, {}, password, encryptHeader, compressionLevel);
QStringList replacedArgs = rarPlugin->substituteAddVariables(addArgs, {}, password, encryptHeader, compressionLevel, 0);
QFETCH(QStringList, expectedArgs);
QCOMPARE(replacedArgs, expectedArgs);
......
......@@ -106,7 +106,7 @@ void CliZipTest::testAddArgs()
QFETCH(QString, password);
QFETCH(int, compressionLevel);
QStringList replacedArgs = plugin->substituteAddVariables(addArgs, {}, password, false, compressionLevel);
QStringList replacedArgs = plugin->substituteAddVariables(addArgs, {}, password, false, compressionLevel, 0);
QFETCH(QStringList, expectedArgs);
QCOMPARE(replacedArgs, expectedArgs);
......
......@@ -88,6 +88,7 @@ QStringList AddDialog::selectedFiles() const
CompressionOptions AddDialog::compressionOptions() const
{
qCDebug(ARK) << "Returning with options:" << m_compOptions;
return m_compOptions;
}
......@@ -100,6 +101,7 @@ void AddDialog::slotOpenOptions()
CompressionOptionsWidget *optionsWidget = new CompressionOptionsWidget(optionsDialog, m_compOptions);
optionsWidget->setMimeType(m_mimeType);
optionsWidget->setEncryptionVisible(false);
optionsWidget->collapsibleMultiVolume->setVisible(false);
optionsWidget->collapsibleCompression->expand();
vlayout->addWidget(optionsWidget);
......
......@@ -126,6 +126,7 @@ Archive::Archive(ReadOnlyArchiveInterface *archiveInterface, bool isReadOnly, QO
, m_hasBeenListed(false)
, m_isReadOnly(isReadOnly)
, m_isSingleFolderArchive(false)
, m_isMultiVolume(false)
, m_extractedFilesSize(0)
, m_error(NoError)
, m_encryptionType(Unencrypted)
......@@ -232,6 +233,11 @@ bool Archive::isMultiVolume() const
return m_iface->isMultiVolume();
}
void Archive::setMultiVolume(bool value)
{
m_iface->setMultiVolume(value);
}
int Archive::numberOfVolumes() const
{
return m_iface->numberOfVolumes();
......@@ -495,4 +501,9 @@ CompressionOptions Archive::compressionOptions() const
return m_compOptions;
}
QString Archive::multiVolumeName() const
{
return m_iface->multiVolumeName();
}
} // namespace Kerfuffle
......@@ -151,7 +151,7 @@ class KERFUFFLE_EXPORT Archive : public QObject
Q_PROPERTY(QMimeType mimeType READ mimeType CONSTANT)
Q_PROPERTY(bool isReadOnly READ isReadOnly CONSTANT)
Q_PROPERTY(bool isSingleFolderArchive READ isSingleFolderArchive)
Q_PROPERTY(bool isMultiVolume READ isMultiVolume)
Q_PROPERTY(bool isMultiVolume READ isMultiVolume WRITE setMultiVolume)
Q_PROPERTY(bool numberOfVolumes READ numberOfVolumes)
Q_PROPERTY(EncryptionType encryptionType READ encryptionType)
Q_PROPERTY(qulonglong numberOfFiles READ numberOfFiles)
......@@ -176,8 +176,9 @@ public:
QMimeType mimeType();
bool isReadOnly() const;
bool isSingleFolderArchive();
bool hasComment() const;
bool isMultiVolume() const;
void setMultiVolume(bool value);
bool hasComment() const;
int numberOfVolumes() const;
EncryptionType encryptionType();
QString password() const;
......@@ -188,6 +189,7 @@ public:
QString subfolderName();
void setCompressionOptions(const CompressionOptions &opts);
CompressionOptions compressionOptions() const;
QString multiVolumeName() const;
static Archive *create(const QString &fileName, QObject *parent = 0);
static Archive *create(const QString &fileName, const QString &fixedMimeType, QObject *parent = 0);
......@@ -257,6 +259,7 @@ private:
bool m_hasBeenListed;
bool m_isReadOnly;
bool m_isSingleFolderArchive;
bool m_isMultiVolume;
QString m_subfolderName;
qulonglong m_extractedFilesSize;
......
......@@ -39,14 +39,16 @@ ArchiveFormat::ArchiveFormat(const QMimeType& mimeType,
int maxCompLevel,
int defaultCompLevel,
bool supportsWriteComment,
bool supportsTesting) :
bool supportsTesting,
bool supportsMultiVolume) :
m_mimeType(mimeType),
m_encryptionType(encryptionType),
m_minCompressionLevel(minCompLevel),
m_maxCompressionLevel(maxCompLevel),
m_defaultCompressionLevel(defaultCompLevel),
m_supportsWriteComment(supportsWriteComment),
m_supportsTesting(supportsTesting)
m_supportsTesting(supportsTesting),
m_supportsMultiVolume(supportsMultiVolume)
{
}
......@@ -66,6 +68,7 @@ ArchiveFormat ArchiveFormat::fromMetadata(const QMimeType& mimeType, const KPlug
bool supportsWriteComment = formatProps[QStringLiteral("SupportsWriteComment")].toBool();
bool supportsTesting = formatProps[QStringLiteral("SupportsTesting")].toBool();
bool supportsMultiVolume = formatProps[QStringLiteral("SupportsMultiVolume")].toBool();
Archive::EncryptionType encType = Archive::Unencrypted;
if (formatProps[QStringLiteral("HeaderEncryption")].toBool()) {
......@@ -74,7 +77,7 @@ ArchiveFormat ArchiveFormat::fromMetadata(const QMimeType& mimeType, const KPlug
encType = Archive::Encrypted;
}
return ArchiveFormat(mimeType, encType, minCompLevel, maxCompLevel, defaultCompLevel, supportsWriteComment, supportsTesting);
return ArchiveFormat(mimeType, encType, minCompLevel, maxCompLevel, defaultCompLevel, supportsWriteComment, supportsTesting, supportsMultiVolume);
}
return ArchiveFormat();
......@@ -115,4 +118,9 @@ bool ArchiveFormat::supportsTesting() const
return m_supportsTesting;
}
bool ArchiveFormat::supportsMultiVolume() const
{
return m_supportsMultiVolume;
}
}
......@@ -43,7 +43,8 @@ public:
int maxCompLevel,
int defaultCompLevel,
bool supportsWriteComment,
bool supportsTesting);
bool supportsTesting,
bool suppportsMultiVolume);
/**
* @return The archive format of the given @p mimeType, according to the given @p metadata.
......@@ -65,6 +66,7 @@ public:
int defaultCompressionLevel() const;
bool supportsWriteComment() const;
bool supportsTesting() const;
bool supportsMultiVolume() const;
private:
QMimeType m_mimeType;
......@@ -74,6 +76,7 @@ private:
int m_defaultCompressionLevel;
bool m_supportsWriteComment;
bool m_supportsTesting;
bool m_supportsMultiVolume;
};
}
......
......@@ -120,11 +120,21 @@ bool ReadOnlyArchiveInterface::isMultiVolume() const
return m_isMultiVolume;
}
void ReadOnlyArchiveInterface::setMultiVolume(bool value)
{
m_isMultiVolume = value;
}
int ReadOnlyArchiveInterface::numberOfVolumes() const
{
return m_numberOfVolumes;
}
QString ReadOnlyArchiveInterface::multiVolumeName() const
{
return filename();
}
ReadWriteArchiveInterface::ReadWriteArchiveInterface(QObject *parent, const QVariantList & args)
: ReadOnlyArchiveInterface(parent, args)
{
......
......@@ -106,6 +106,8 @@ public:
virtual bool doResume();
bool isHeaderEncryptionEnabled() const;
virtual QString multiVolumeName() const;
void setMultiVolume(bool value);
signals:
void cancelled();
......@@ -129,7 +131,6 @@ protected:
void setCorrupt(bool isCorrupt);
bool isCorrupt() const;
QString m_comment;
bool m_isMultiVolume;
int m_numberOfVolumes;
private:
......@@ -138,6 +139,7 @@ private:
bool m_waitForFinishedSignal;
bool m_isHeaderEncryptionEnabled;
bool m_isCorrupt;
bool m_isMultiVolume;
};
class KERFUFFLE_EXPORT ReadWriteArchiveInterface: public ReadOnlyArchiveInterface
......
......@@ -46,6 +46,7 @@
#include <QDirIterator>
#include <QEventLoop>
#include <QFile>
#include <QMimeDatabase>
#include <QProcess>
#include <QRegularExpression>
#include <QStandardPaths>
......@@ -182,12 +183,14 @@ bool CliInterface::addFiles(const QStringList & files, const CompressionOptions&
}
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,
password(),
isHeaderEncryptionEnabled(),
compLevel);
compLevel,
volumeSize);
if (!runProcess(m_param.value(AddProgram).toStringList(), args)) {
return false;
......@@ -303,7 +306,7 @@ void CliInterface::processFinished(int exitCode, QProcess::ExitStatus exitStatus
}
}
if (m_operationMode == Add) {
if (m_operationMode == Add && !isMultiVolume()) {
list();
} else if (m_operationMode == List && isCorrupt()) {
Kerfuffle::LoadCorruptQuery query(filename());
......@@ -635,7 +638,7 @@ QStringList CliInterface::substituteCopyVariables(const QStringList &extractArgs
return args;
}
QStringList CliInterface::substituteAddVariables(const QStringList &addArgs, const QStringList &files, 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)
{
// Required if we call this function from unit tests.
cacheParameterList();
......@@ -659,6 +662,11 @@ QStringList CliInterface::substituteAddVariables(const QStringList &addArgs, con
continue;
}
if (arg == QLatin1String("$MultiVolumeSwitch")) {
args << multiVolumeSwitch(volumeSize);
continue;
}
if (arg == QLatin1String("$Files")) {
args << files;
continue;
......@@ -832,6 +840,24 @@ QString CliInterface::compressionLevelSwitch(int level) const
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 filesList;
......@@ -1261,4 +1287,20 @@ bool CliInterface::addComment(const QString &comment)
return true;
}
QString CliInterface::multiVolumeName() const
{
QString oldSuffix = QMimeDatabase().suffixForFileName(filename());
QString name;
foreach (const QString &multiSuffix, m_param.value(MultiVolumeSuffix).toStringList()) {
QString newSuffix = multiSuffix;
newSuffix.replace(QStringLiteral("$Suffix"), oldSuffix);
name = filename().remove(oldSuffix).append(newSuffix);
if (QFileInfo(name).exists()) {
break;
}
}
return name;
}
}
......@@ -263,7 +263,9 @@ enum CliInterfaceParameters {
CommentSwitch,
TestProgram,
TestArgs,
TestPassedPattern
TestPassedPattern,
MultiVolumeSwitch,
MultiVolumeSuffix
};
typedef QHash<int, QVariant> ParameterList;
......@@ -310,7 +312,7 @@ public:
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);
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 substituteCommentVariables(const QStringList &commentArgs, const QString &commentFile);
QStringList substituteTestVariables(const QStringList &testArgs, const QString &password);
......@@ -335,11 +337,15 @@ public:
*/
QString compressionLevelSwitch(int level) const;
QString multiVolumeSwitch(ulong volumeSize) const;
/**
* @return The list of selected files to extract.
*/
QStringList copyFilesList(const QVariantList& files) const;
QString multiVolumeName() const;
protected:
virtual void handleLine(const QString& line);
......
......@@ -47,12 +47,24 @@ CompressionOptionsWidget::CompressionOptionsWidget(QWidget *parent,
KColorScheme colorScheme(QPalette::Active, KColorScheme::View);
pwdWidget->setBackgroundWarningColor(colorScheme.background(KColorScheme::NegativeBackground).color());
pwdWidget->setPasswordStrengthMeterVisible(false);
connect(multiVolumeCheckbox, &QCheckBox::stateChanged, this, &CompressionOptionsWidget::slotMultiVolumeChecked);
if (m_opts.contains(QStringLiteral("VolumeSize"))) {
multiVolumeCheckbox->setChecked(true);
// Convert from kilobytes.
volumeSizeSpinbox->setValue(m_opts.value(QStringLiteral("VolumeSize")).toDouble() / 1024);
}
}
CompressionOptions CompressionOptionsWidget::commpressionOptions() const
{
CompressionOptions opts;
opts[QStringLiteral("CompressionLevel")] = compLevelSlider->value();
if (multiVolumeCheckbox->isChecked()) {
// Convert to kilobytes.
opts[QStringLiteral("VolumeSize")] = QString::number(volumeSize());
}
return opts;
}
......@@ -62,6 +74,16 @@ int CompressionOptionsWidget::compressionLevel() const
return compLevelSlider->value();
}
ulong CompressionOptionsWidget::volumeSize() const
{
if (collapsibleMultiVolume->isEnabled() && multiVolumeCheckbox->isChecked()) {
// Convert to kilobytes.
return volumeSizeSpinbox->value() * 1024;
} else {
return 0;
}
}
void CompressionOptionsWidget::setEncryptionVisible(bool visible)
{
collapsibleEncryption->setVisible(visible);
......@@ -122,6 +144,15 @@ void CompressionOptionsWidget::updateWidgets()
compLevelSlider->setValue(archiveFormat.defaultCompressionLevel());
}
}
if (archiveFormat.supportsMultiVolume()) {
collapsibleMultiVolume->setEnabled(true);
collapsibleMultiVolume->setToolTip(QString());
} else {
collapsibleMultiVolume->setEnabled(false);
collapsibleMultiVolume->setToolTip(i18n("The %1 format does not support multi-volume archives.",
m_mimetype.comment()));
}
}
void CompressionOptionsWidget::setMimeType(const QMimeType &mimeType)
......@@ -155,4 +186,15 @@ KNewPasswordWidget::PasswordStatus CompressionOptionsWidget::passwordStatus() co
return pwdWidget->passwordStatus();
}
void CompressionOptionsWidget::slotMultiVolumeChecked(int state)
{
if (state == Qt::Checked) {
lblVolumeSize->setEnabled(true);
volumeSizeSpinbox->setEnabled(true);
} else {
lblVolumeSize->setEnabled(false);
volumeSizeSpinbox->setEnabled(false);
}
}
}
......@@ -45,6 +45,7 @@ public:
explicit CompressionOptionsWidget(QWidget *parent = Q_NULLPTR,
const CompressionOptions &opts = QHash<QString, QVariant>());
int compressionLevel() const;
ulong volumeSize() const;
QString password() const;
CompressionOptions commpressionOptions() const;
bool isEncryptionAvailable() const;
......@@ -61,6 +62,9 @@ private:
QMimeType m_mimetype;
CompressionOptions m_opts;
private slots:
void slotMultiVolumeChecked(int state);
};
}
......
......@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>384</width>
<height>62</height>
<width>401</width>
<height>90</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
......@@ -106,7 +106,7 @@
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="KNewPasswordWidget" name="pwdWidget" native="true">
<widget class="KNewPasswordWidget" name="pwdWidget">
<property name="enabled">
<bool>false</bool>
</property>
......@@ -131,6 +131,73 @@
</layout>
</widget>
</item>
<item>
<widget class="KCollapsibleGroupBox" name="collapsibleMultiVolume">
<property name="title">
<string>Multi-volume Archive</string>
</property>
<property name="expanded">
<bool>false</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QCheckBox" name="multiVolumeCheckbox">
<property name="text">
<string>Create multi-volume archive</string>
</property>
</widget>
</item>
<item>
<layout class="QFormLayout" name="formLayout">
<property name="verticalSpacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>30</number>
</property>
<item row="1" column="0">
<widget class="QLabel" name="lblVolumeSize">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Volume size:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="volumeSizeSpinbox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="suffix">
<string> megabytes</string>
</property>
<property name="decimals">
<number>1</number>
</property>
<property name="minimum">
<double>0.100000000000000</double>
</property>
<property name="maximum">
<double>1000.000000000000000</double>
</property>
<property name="singleStep">
<double>0.500000000000000</double>
</property>
<property name="value">
<double>1.000000000000000</double>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<customwidgets>
......@@ -143,7 +210,7 @@
<customwidget>
<class>KNewPasswordWidget</class>
<extends>QWidget</extends>
<header location="global">KNewPasswordWidget</header>
<header>knewpasswordwidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
......
......@@ -151,6 +151,11 @@ int CreateDialog::compressionLevel() const
return m_ui->optionsWidget->compressionLevel();
}
ulong CreateDialog::volumeSize() const
{
return m_ui->optionsWidget->volumeSize();
}
QString CreateDialog::password() const
{
return m_ui->optionsWidget->password();
......
......@@ -59,6 +59,7 @@ public:
QMimeType currentMimeType() const;
bool setMimeType(const QString &mimeTypeName);
int compressionLevel() const;
ulong volumeSize() const;
/**
* @return Whether the user can encrypt the new archive.
......
......@@ -409,7 +409,8 @@ void Part::setupActions()
void Part::updateActions()
{
bool isWritable = m_model->archive() && !m_model->archive()->isReadOnly();
bool isWritable = m_model->archive() && !m_model->archive()->isReadOnly() &&
!(m_model->rowCount() > 0 && m_model->archive()->isMultiVolume());
bool isDirectory = m_model->entryForIndex(m_view->selectionModel()->currentIndex())[IsDirectory].toBool();
int selectedEntriesCount = m_view->selectionModel()->selectedRows().count();
......@@ -663,6 +664,10 @@ bool Part::openFile()
Q_ASSERT(archive->isValid());
if (arguments().metaData().contains(QStringLiteral("volumeSize"))) {
archive.data()->setMultiVolume(true);
}
// Plugin loaded successfully.
KJob *job = m_model->setArchive(archive.take());
if (job) {
......@@ -1243,6 +1248,9 @@ void Part::slotAddFiles()
if (arguments().metaData().contains(QStringLiteral("compressionLevel"))) {
opts[QStringLiteral("CompressionLevel")] = arguments().metaData()[QStringLiteral("compressionLevel")];
}
if (arguments().metaData().contains(QStringLiteral("volumeSize"))) {
opts[QStringLiteral("VolumeSize")] = arguments().metaData()[QStringLiteral("volumeSize")];
}
m_model->archive()->setCompressionOptions(opts);
} else {
opts = m_model->archive()->compressionOptions();
......@@ -1282,6 +1290,17 @@ 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()));
}
}
}
......
......@@ -79,6 +79,7 @@ ParameterList CliPlugin::parameterList() const
<< QStringLiteral("$Archive")
<< QStringLiteral("$PasswordSwitch")
<< QStringLiteral("$CompressionLevelSwitch")
<< QStringLiteral("$MultiVolumeSwitch")
<< QStringLiteral("$Files");