File indexing completed on 2024-03-24 17:23:01

0001 // SPDX-FileCopyrightText: 2020 Simon Persson <simon.persson@mykolab.com>
0002 //
0003 // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0004 
0005 #include "kupkcm.h"
0006 #include "backupplan.h"
0007 #include "backupplanwidget.h"
0008 #include "kupdaemon.h"
0009 #include "kupsettings.h"
0010 #include "planstatuswidget.h"
0011 
0012 #include <QCheckBox>
0013 #include <QDBusInterface>
0014 #include <QLabel>
0015 #include <QPushButton>
0016 #include <QScrollArea>
0017 #include <QStackedLayout>
0018 
0019 #include <KAboutData>
0020 #include <KConfigDialogManager>
0021 #include <KLineEdit>
0022 #include <KLocalizedString>
0023 #include <KMessageBox>
0024 #include <KPluginFactory>
0025 #include <KProcess>
0026 
0027 #if QT_VERSION_MAJOR == 5
0028 #include <Kdelibs4ConfigMigrator>
0029 #endif
0030 
0031 K_PLUGIN_CLASS_WITH_JSON(KupKcm, "kcm_kup.json")
0032 
0033 #if QT_VERSION_MAJOR == 5
0034 KupKcm::KupKcm(QWidget *pParent, const QVariantList &pArgs)
0035     : KCModule(pParent, pArgs)
0036 #else
0037     KupKcm::KupKcm(QObject *pParent, const KPluginMetaData &md, const QVariantList &pArgs)
0038     : KCModule(pParent, md)
0039 #endif
0040     , mSourcePageToShow(0)
0041 {
0042 #if QT_VERSION_MAJOR == 5
0043     KAboutData lAbout(QStringLiteral("kcm_kup"), i18n("Kup Configuration Module"),
0044                       QStringLiteral("0.9.1"),
0045                       i18n("Configuration of backup plans for the Kup backup system"),
0046                       KAboutLicense::GPL, i18n("Copyright (C) 2011-2020 Simon Persson"));
0047     lAbout.addAuthor(i18n("Simon Persson"), i18n("Maintainer"), "simon.persson@mykolab.com");
0048     lAbout.setTranslator(xi18nc("NAME OF TRANSLATORS", "Your names"),
0049                          xi18nc("EMAIL OF TRANSLATORS", "Your emails"));
0050     setAboutData(new KAboutData(lAbout));
0051 #endif
0052     setObjectName(QStringLiteral("kcm_kup")); //needed for the kconfigdialogmanager magic
0053     setButtons((Apply | buttons()) & ~Default);
0054 
0055     KProcess lBupProcess;
0056     lBupProcess << QStringLiteral("bup") << QStringLiteral("version");
0057     lBupProcess.setOutputChannelMode(KProcess::MergedChannels);
0058     int lExitCode = lBupProcess.execute();
0059     if(lExitCode >= 0) {
0060         mBupVersion = QString::fromUtf8(lBupProcess.readAllStandardOutput());
0061         KProcess lPar2Process;
0062         lPar2Process << QStringLiteral("bup") << QStringLiteral("fsck") << QStringLiteral("--par2-ok");
0063         mPar2Available = lPar2Process.execute() == 0;
0064     } else {
0065         mPar2Available = false;
0066     }
0067 
0068     KProcess lRsyncProcess;
0069     lRsyncProcess << QStringLiteral("rsync") << QStringLiteral("--version");
0070     lRsyncProcess.setOutputChannelMode(KProcess::MergedChannels);
0071     lExitCode = lRsyncProcess.execute();
0072     if(lExitCode >= 0) {
0073         QString lOutput = QString::fromLocal8Bit(lRsyncProcess.readLine());
0074         mRsyncVersion = lOutput.split(QLatin1Char(' '), Qt::SkipEmptyParts).at(2);
0075     }
0076 
0077     if(mBupVersion.isEmpty() && mRsyncVersion.isEmpty()) {
0078         auto lSorryIcon = new QLabel;
0079         lSorryIcon->setPixmap(QIcon::fromTheme(QStringLiteral("dialog-error")).pixmap(64, 64));
0080         QString lInstallMessage = i18n("<h2>Backup programs are missing</h2>"
0081                                        "<p>Before you can activate any backup plan you need to "
0082                                        "install either of</p>"
0083                                        "<ul><li>bup, for versioned backups</li>"
0084                                        "<li>rsync, for synchronized backups</li></ul>");
0085         auto lSorryText = new QLabel(lInstallMessage);
0086         lSorryText->setWordWrap(true);
0087         auto lHLayout = new QHBoxLayout;
0088         lHLayout->addWidget(lSorryIcon);
0089         lHLayout->addWidget(lSorryText, 1);
0090 #if QT_VERSION_MAJOR == 5
0091         setLayout(lHLayout);
0092 #else
0093         widget()->setLayout(lHLayout);
0094 #endif
0095     } else {
0096 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0097         Kdelibs4ConfigMigrator lMigrator(QStringLiteral("kup"));
0098         lMigrator.setConfigFiles(QStringList() << QStringLiteral("kuprc"));
0099         lMigrator.migrate();
0100 #endif
0101 
0102         mConfig = KSharedConfig::openConfig(QStringLiteral("kuprc"));
0103         mSettings = new KupSettings(mConfig, this);
0104         for(int i = 0; i < mSettings->mNumberOfPlans; ++i) {
0105             mPlans.append(new BackupPlan(i+1, mConfig, this));
0106             mConfigManagers.append(nullptr);
0107             mPlanWidgets.append(nullptr);
0108             mStatusWidgets.append(nullptr);
0109         }
0110         createSettingsFrontPage();
0111         addConfig(mSettings, mFrontPage);
0112         mStackedLayout = new QStackedLayout;
0113         mStackedLayout->addWidget(mFrontPage);
0114 #if QT_VERSION_MAJOR == 5
0115         setLayout(mStackedLayout);
0116 #else
0117         widget()->setLayout(mStackedLayout);
0118 #endif
0119         QListIterator<QVariant> lIter(pArgs);
0120         while(lIter.hasNext()) {
0121             QVariant lVariant = lIter.next();
0122             if(lVariant.type() == QVariant::String) {
0123                 QString lArgument = lVariant.toString();
0124                 if(lArgument == QStringLiteral("show_sources") && lIter.hasNext()) {
0125                     mSourcePageToShow = lIter.next().toString().toInt();
0126                 }
0127             }
0128         }
0129     }
0130 }
0131 
0132 #if QT_VERSION_MAJOR == 5
0133 QSize KupKcm::sizeHint() const {
0134     return {800, 600};
0135 }
0136 #endif
0137 
0138 void KupKcm::load() {
0139     if(mBupVersion.isEmpty() && mRsyncVersion.isEmpty()) {
0140         return;
0141     }
0142     // status will be set correctly after construction, set to checked here to
0143     // match the enabled status of other widgets
0144     mEnableCheckBox->setChecked(true);
0145     for(int i = 0; i < mSettings->mNumberOfPlans; ++i) {
0146         if(!mConfigManagers.at(i))
0147             createPlanWidgets(i);
0148         mConfigManagers.at(i)->updateWidgets();
0149     }
0150     for(int i = mSettings->mNumberOfPlans; i < mPlans.count();) {
0151         completelyRemovePlan(i);
0152     }
0153     KCModule::load();
0154     // this call is needed because it could have been set true before, now load() is called
0155     // because user pressed reset button. need to manually reset the "changed" state to false
0156     // in this case.
0157     unmanagedWidgetChangeState(false);
0158     if(mSourcePageToShow > 0) {
0159         mStackedLayout->setCurrentIndex(mSourcePageToShow);
0160         mPlanWidgets[mSourcePageToShow - 1]->showSourcePage();
0161         mSourcePageToShow = 0; //only trigger on first load after startup.
0162     }
0163 }
0164 
0165 void KupKcm::save() {
0166     int lPlansRemoved = 0;
0167     for(int i=0; i < mPlans.count(); ++i) {
0168         auto *lPlan = mPlans.at(i);
0169         auto *lManager = mConfigManagers.at(i);
0170         if(lManager == nullptr) {
0171             lPlan->removePlanFromConfig();
0172             delete mPlans.takeAt(i);
0173             mConfigManagers.removeAt(i);
0174             mStatusWidgets.removeAt(i);
0175             delete mPlanWidgets.takeAt(i);
0176             ++lPlansRemoved;
0177             --i;
0178             continue;
0179         }
0180         if(lPlansRemoved != 0) {
0181             lPlan->removePlanFromConfig();
0182             lPlan->setPlanNumber(i + 1);
0183             // config manager does not detect a changed group name of the config items.
0184             // To work around, read default settings - config manager will then notice
0185             // changed values and save current widget status into the config using the
0186             // new group name. If all settings for the plan already was default then
0187             // nothing was saved anyway, either under old or new group name.
0188             lPlan->setDefaults();
0189             lPlan->save();
0190         }
0191         mPlanWidgets.at(i)->saveExtraData();
0192         lManager->updateSettings();
0193         mStatusWidgets.at(i)->updateIcon();
0194         if(lPlan->mDestinationType == 1 && lPlan->mExternalUUID.isEmpty()) {
0195 #if QT_VERSION_MAJOR == 5
0196             QWidget *wid = this;
0197 #else
0198             QWidget *wid = widget();
0199 #endif
0200             KMessageBox::information(wid, xi18nc("@info %1 is the name of the backup plan",
0201                                                   "%1 does not have a destination!<nl/>"
0202                                                   "No backups will be saved by this plan.",
0203                                                   lPlan->mDescription),
0204                                      xi18nc("@title:window", "Warning"),
0205                                      QString(), KMessageBox::Dangerous);
0206         }
0207     }
0208     mSettings->mNumberOfPlans = mPlans.count();
0209     mSettings->save();
0210 
0211     KCModule::save();
0212 
0213     QDBusInterface lInterface(KUP_DBUS_SERVICE_NAME, KUP_DBUS_OBJECT_PATH);
0214     if(lInterface.isValid()) {
0215         lInterface.call(QStringLiteral("reloadConfig"));
0216     } else {
0217         KProcess::startDetached(QStringLiteral("kup-daemon"));
0218     }
0219 }
0220 
0221 void KupKcm::updateChangedStatus() {
0222     bool lHasUnmanagedChanged = false;
0223     foreach(KConfigDialogManager *lConfigManager, mConfigManagers) {
0224         if(!lConfigManager || lConfigManager->hasChanged()) {
0225             lHasUnmanagedChanged = true;
0226             break;
0227         }
0228     }
0229     if(mPlanWidgets.count() != mSettings->mNumberOfPlans)
0230         lHasUnmanagedChanged = true;
0231     unmanagedWidgetChangeState(lHasUnmanagedChanged);
0232 }
0233 
0234 void KupKcm::showFrontPage() {
0235     mStackedLayout->setCurrentIndex(0);
0236 }
0237 
0238 void KupKcm::createSettingsFrontPage() {
0239     mFrontPage = new QWidget;
0240     auto lHLayout = new QHBoxLayout;
0241     auto lVLayout = new QVBoxLayout;
0242     auto lScrollArea = new QScrollArea;
0243     auto lCentralWidget = new QWidget(lScrollArea);
0244     mVerticalLayout = new QVBoxLayout;
0245     lScrollArea->setWidget(lCentralWidget);
0246     lScrollArea->setWidgetResizable(true);
0247     lScrollArea->setFrameStyle(QFrame::NoFrame);
0248 
0249     auto lAddPlanButton = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")),
0250                                                   xi18nc("@action:button", "Add New Plan"));
0251     connect(lAddPlanButton, &QPushButton::clicked, this, [this]{
0252         mPlans.append(new BackupPlan(mPlans.count() + 1, mConfig, this));
0253         if(mBupVersion.isEmpty()) mPlans.last()->mBackupType = 1;
0254         mConfigManagers.append(nullptr);
0255         mPlanWidgets.append(nullptr);
0256         mStatusWidgets.append(nullptr);
0257         createPlanWidgets(mPlans.count() - 1);
0258         updateChangedStatus();
0259         emit mStatusWidgets.at(mPlans.count() - 1)->configureMe();
0260     });
0261 
0262     mEnableCheckBox = new QCheckBox(xi18nc("@option:check", "Backups Enabled"));
0263     mEnableCheckBox->setObjectName(QStringLiteral("kcfg_Backups enabled"));
0264     connect(mEnableCheckBox, &QCheckBox::toggled, lAddPlanButton, &QPushButton::setEnabled);
0265 
0266     lHLayout->addWidget(mEnableCheckBox);
0267     lHLayout->addStretch();
0268     lHLayout->addWidget(lAddPlanButton);
0269     lVLayout->addLayout(lHLayout);
0270     lVLayout->addWidget(lScrollArea);
0271     mFrontPage->setLayout(lVLayout);
0272 
0273     auto lFilediggerButton = new QPushButton(xi18nc("@action:button", "Open and restore from existing backups"));
0274     connect(lFilediggerButton, &QPushButton::clicked, []{KProcess::startDetached(QStringLiteral("kup-filedigger"));});
0275     mVerticalLayout->addWidget(lFilediggerButton);
0276     mVerticalLayout->addStretch(1);
0277     lCentralWidget->setLayout(mVerticalLayout);
0278 }
0279 
0280 void KupKcm::createPlanWidgets(int pIndex) {
0281     auto lPlanWidget = new BackupPlanWidget(mPlans.at(pIndex), mBupVersion,
0282                                              mRsyncVersion, mPar2Available);
0283     connect(lPlanWidget, SIGNAL(requestOverviewReturn()), this, SLOT(showFrontPage()));
0284     auto lConfigManager = new KConfigDialogManager(lPlanWidget, mPlans.at(pIndex));
0285     lConfigManager->setObjectName(objectName());
0286     connect(lConfigManager, SIGNAL(widgetModified()), this, SLOT(updateChangedStatus()));
0287     auto lStatusWidget = new PlanStatusWidget(mPlans.at(pIndex));
0288     connect(lStatusWidget, &PlanStatusWidget::removeMe, this, [this]{
0289         int lIndex = mStatusWidgets.indexOf(qobject_cast<PlanStatusWidget*>(sender()));
0290         if(lIndex < mSettings->mNumberOfPlans)
0291             partiallyRemovePlan(lIndex);
0292         else
0293             completelyRemovePlan(lIndex);
0294         updateChangedStatus();
0295     });
0296     connect(lStatusWidget, &PlanStatusWidget::configureMe, this, [this]{
0297         int lIndex = mStatusWidgets.indexOf(qobject_cast<PlanStatusWidget*>(sender()));
0298         mStackedLayout->setCurrentIndex(lIndex + 1);
0299     });
0300     connect(lStatusWidget, &PlanStatusWidget::duplicateMe, this, [this]{
0301         int lIndex = mStatusWidgets.indexOf(qobject_cast<PlanStatusWidget*>(sender()));
0302         auto lNewPlan = new BackupPlan(mPlans.count() + 1, mConfig, this);
0303         lNewPlan->copyFrom(*mPlans.at(lIndex));
0304         mPlans.append(lNewPlan);
0305         mConfigManagers.append(nullptr);
0306         mPlanWidgets.append(nullptr);
0307         mStatusWidgets.append(nullptr);
0308         createPlanWidgets(mPlans.count() - 1);
0309         // crazy trick to make the config system realize that stuff has changed
0310         // and will need to be saved.
0311         lNewPlan->setDefaults();
0312         updateChangedStatus();
0313     });
0314     connect(mEnableCheckBox, &QCheckBox::toggled,
0315             lStatusWidget, &PlanStatusWidget::setEnabled);
0316     connect(lPlanWidget->mDescriptionEdit, &KLineEdit::textChanged,
0317             lStatusWidget->mDescriptionLabel, &QLabel::setText);
0318 
0319     mConfigManagers[pIndex] = lConfigManager;
0320     mPlanWidgets[pIndex] = lPlanWidget;
0321     mStackedLayout->insertWidget(pIndex + 1, lPlanWidget);
0322     mStatusWidgets[pIndex] = lStatusWidget;
0323     // always insert at end, before the file digger button and strech space at the bottom.
0324     mVerticalLayout->insertWidget(mVerticalLayout->count() - 2, lStatusWidget);
0325 }
0326 
0327 void KupKcm::completelyRemovePlan(int pIndex) {
0328     delete mConfigManagers.takeAt(pIndex);
0329     delete mStatusWidgets.takeAt(pIndex);
0330     delete mPlanWidgets.takeAt(pIndex);
0331     delete mPlans.takeAt(pIndex);
0332 }
0333 
0334 void KupKcm::partiallyRemovePlan(int pIndex) {
0335     mConfigManagers.at(pIndex)->deleteLater();
0336     mConfigManagers[pIndex] = nullptr;
0337     mStatusWidgets.at(pIndex)->deleteLater();
0338     mStatusWidgets[pIndex] = nullptr;
0339 }
0340 
0341 #include "kupkcm.moc"