File indexing completed on 2022-09-27 16:29:04

0001 /**
0002  * SPDX-FileCopyrightText: (C) 2003 by Sébastien Laoût <slaout@linux62.org>
0003  * SPDX-License-Identifier: GPL-2.0-or-later
0004  */
0005 
0006 #include "backup.h"
0007 
0008 #include "formatimporter.h" // To move a folder
0009 #include "global.h"
0010 #include "settings.h"
0011 #include "tools.h"
0012 #include "variouswidgets.h"
0013 
0014 #include <QApplication>
0015 #include <QDialogButtonBox>
0016 #include <QFileDialog>
0017 #include <QGroupBox>
0018 #include <QHBoxLayout>
0019 #include <QLabel>
0020 #include <QLayout>
0021 #include <QLocale>
0022 #include <QProgressBar>
0023 #include <QProgressDialog>
0024 #include <QPushButton>
0025 #include <QVBoxLayout>
0026 #include <QtCore/QDir>
0027 #include <QtCore/QTextStream>
0028 
0029 #include <KAboutData>
0030 #include <KConfig>
0031 #include <KConfigGroup>
0032 #include <KIconLoader>
0033 #include <KLocalizedString>
0034 #include <KMessageBox>
0035 #include <KRun>
0036 #include <KTar>
0037 
0038 #include <KIO/CommandLauncherJob>
0039 
0040 #include <unistd.h> // usleep()
0041 
0042 /**
0043  * Backups are wrapped in a .tar.gz, inside that folder name.
0044  * An archive is not a backup or is corrupted if data are not in that folder!
0045  */
0046 const QString backupMagicFolder = "BasKet-Note-Pads_Backup";
0047 
0048 /** class BackupDialog: */
0049 
0050 BackupDialog::BackupDialog(QWidget *parent, const char *name)
0051     : QDialog(parent)
0052 {
0053     setObjectName(name);
0054     setModal(true);
0055     setWindowTitle(i18n("Backup & Restore"));
0056 
0057     QWidget *mainWidget = new QWidget(this);
0058     QVBoxLayout *mainLayout = new QVBoxLayout;
0059     setLayout(mainLayout);
0060     mainLayout->addWidget(mainWidget);
0061 
0062     QWidget *page = new QWidget(this);
0063     QVBoxLayout *pageVBoxLayout = new QVBoxLayout(page);
0064     pageVBoxLayout->setMargin(0);
0065     mainLayout->addWidget(page);
0066 
0067     //  pageVBoxLayout->setSpacing(spacingHint());
0068 
0069     QString savesFolder = Global::savesFolder();
0070     savesFolder = savesFolder.left(savesFolder.length() - 1); // savesFolder ends with "/"
0071 
0072     QGroupBox *folderGroup = new QGroupBox(i18n("Save Folder"), page);
0073     pageVBoxLayout->addWidget(folderGroup);
0074     mainLayout->addWidget(folderGroup);
0075     QVBoxLayout *folderGroupLayout = new QVBoxLayout;
0076     folderGroup->setLayout(folderGroupLayout);
0077     folderGroupLayout->addWidget(new QLabel("<qt><nobr>" + i18n("Your baskets are currently stored in that folder:<br><b>%1</b>", savesFolder), folderGroup));
0078     QWidget *folderWidget = new QWidget;
0079     folderGroupLayout->addWidget(folderWidget);
0080 
0081     QHBoxLayout *folderLayout = new QHBoxLayout(folderWidget);
0082     folderLayout->setContentsMargins(0, 0, 0, 0);
0083 
0084     QPushButton *moveFolder = new QPushButton(i18n("&Move to Another Folder..."), folderWidget);
0085     QPushButton *useFolder = new QPushButton(i18n("&Use Another Existing Folder..."), folderWidget);
0086     HelpLabel *helpLabel = new HelpLabel(i18n("Why to do that?"),
0087                                          i18n("<p>You can move the folder where %1 store your baskets to:</p><ul>"
0088                                               "<li>Store your baskets in a visible place in your home folder, like ~/Notes or ~/Baskets, so you can manually backup them when you want.</li>"
0089                                               "<li>Store your baskets on a server to share them between two computers.<br>"
0090                                               "In this case, mount the shared-folder to the local file system and ask %1 to use that mount point.<br>"
0091                                               "Warning: you should not run %1 at the same time on both computers, or you risk to loss data while the two applications are desynced.</li>"
0092                                               "</ul><p>Please remember that you should not change the content of that folder manually (eg. adding a file in a basket folder will not add that file to the basket).</p>",
0093                                               QGuiApplication::applicationDisplayName()),
0094                                          folderWidget);
0095     folderLayout->addWidget(moveFolder);
0096     folderLayout->addWidget(useFolder);
0097     folderLayout->addWidget(helpLabel);
0098     folderLayout->addStretch();
0099     connect(moveFolder, &QPushButton::clicked, this, &BackupDialog::moveToAnotherFolder);
0100     connect(useFolder, &QPushButton::clicked, this, &BackupDialog::useAnotherExistingFolder);
0101 
0102     QGroupBox *backupGroup = new QGroupBox(i18n("Backups"), page);
0103     pageVBoxLayout->addWidget(backupGroup);
0104     mainLayout->addWidget(backupGroup);
0105     QVBoxLayout *backupGroupLayout = new QVBoxLayout;
0106     backupGroup->setLayout(backupGroupLayout);
0107     QWidget *backupWidget = new QWidget;
0108     backupGroupLayout->addWidget(backupWidget);
0109 
0110     QHBoxLayout *backupLayout = new QHBoxLayout(backupWidget);
0111     backupLayout->setContentsMargins(0, 0, 0, 0);
0112 
0113     QPushButton *backupButton = new QPushButton(i18n("&Backup..."), backupWidget);
0114     QPushButton *restoreButton = new QPushButton(i18n("&Restore a Backup..."), backupWidget);
0115     m_lastBackup = new QLabel(QString(), backupWidget);
0116     backupLayout->addWidget(backupButton);
0117     backupLayout->addWidget(restoreButton);
0118     backupLayout->addWidget(m_lastBackup);
0119     backupLayout->addStretch();
0120     connect(backupButton, &QPushButton::clicked, this, &BackupDialog::backup);
0121     connect(restoreButton, &QPushButton::clicked, this, &BackupDialog::restore);
0122 
0123     populateLastBackup();
0124 
0125     (new QWidget(page))->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
0126 
0127     QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close);
0128     connect(buttonBox, &QDialogButtonBox::rejected, this, &BackupDialog::reject);
0129     mainLayout->addWidget(buttonBox);
0130     buttonBox->button(QDialogButtonBox::Close)->setDefault(true);
0131 }
0132 
0133 BackupDialog::~BackupDialog()
0134 {
0135 }
0136 
0137 void BackupDialog::populateLastBackup()
0138 {
0139     QString lastBackupText = i18n("Last backup: never");
0140     if (Settings::lastBackup().isValid())
0141         lastBackupText = i18n("Last backup: %1", Settings::lastBackup().toString(Qt::LocalDate));
0142 
0143     m_lastBackup->setText(lastBackupText);
0144 }
0145 
0146 void BackupDialog::moveToAnotherFolder()
0147 {
0148     QUrl selectedURL = QFileDialog::getExistingDirectoryUrl(/*parent=*/nullptr,
0149                                                             /*caption=*/i18n("Choose a Folder Where to Move Baskets"),
0150                                                             /*startDir=*/Global::savesFolder());
0151 
0152     if (!selectedURL.isEmpty()) {
0153         QString folder = selectedURL.path();
0154         QDir dir(folder);
0155         // The folder should not exists, or be empty (because KDirSelectDialog will likely create it anyway):
0156         if (dir.exists()) {
0157             // Get the content of the folder:
0158             QStringList content = dir.entryList();
0159             if (content.count() > 2) { // "." and ".."
0160                 int result = KMessageBox::questionYesNo(nullptr, "<qt>" + i18n("The folder <b>%1</b> is not empty. Do you want to overwrite it?", folder), i18n("Overwrite Folder?"), KGuiItem(i18n("&Overwrite"), "document-save"));
0161                 if (result == KMessageBox::No)
0162                     return;
0163             }
0164             Tools::deleteRecursively(folder);
0165         }
0166         FormatImporter copier;
0167         copier.moveFolder(Global::savesFolder(), folder);
0168         Backup::setFolderAndRestart(folder, i18n("Your baskets have been successfully moved to <b>%1</b>. %2 is going to be restarted to take this change into account."));
0169     }
0170 }
0171 
0172 void BackupDialog::useAnotherExistingFolder()
0173 {
0174     QUrl selectedURL = QFileDialog::getExistingDirectoryUrl(/*parent=*/nullptr,
0175                                                             /*caption=*/i18n("Choose an Existing Folder to Store Baskets"),
0176                                                             /*startDir=*/Global::savesFolder());
0177 
0178     if (!selectedURL.isEmpty()) {
0179         Backup::setFolderAndRestart(selectedURL.path(), i18n("Your basket save folder has been successfully changed to <b>%1</b>. %2 is going to be restarted to take this change into account."));
0180     }
0181 }
0182 
0183 void BackupDialog::backup()
0184 {
0185     QDir dir;
0186 
0187     // Compute a default file name & path (eg. "Baskets_2007-01-31.tar.gz"):
0188     KConfig *config = KSharedConfig::openConfig().data();
0189     KConfigGroup configGroup(config, "Backups");
0190     QString folder = configGroup.readEntry("lastFolder", QDir::homePath()) + '/';
0191     QString fileName = i18nc("Backup filename (without extension), %1 is the date", "Baskets_%1", QDate::currentDate().toString(Qt::ISODate));
0192     QString url = folder + fileName;
0193 
0194     // Ask a file name & path to the user:
0195     QString filter = "*.tar.gz|" + i18n("Tar Archives Compressed by Gzip") + "\n*|" + i18n("All Files");
0196     QString destination = url;
0197     for (bool askAgain = true; askAgain;) {
0198         // Ask:
0199         destination = QFileDialog::getSaveFileName(nullptr, i18n("Backup Baskets"), destination, filter);
0200         // User canceled?
0201         if (destination.isEmpty())
0202             return;
0203         // File already existing? Ask for overriding:
0204         if (dir.exists(destination)) {
0205             int result = KMessageBox::questionYesNoCancel(
0206                 nullptr, "<qt>" + i18n("The file <b>%1</b> already exists. Do you really want to overwrite it?", QUrl::fromLocalFile(destination).fileName()), i18n("Overwrite File?"), KGuiItem(i18n("&Overwrite"), "document-save"));
0207             if (result == KMessageBox::Cancel)
0208                 return;
0209             else if (result == KMessageBox::Yes)
0210                 askAgain = false;
0211         } else
0212             askAgain = false;
0213     }
0214 
0215     QProgressDialog dialog;
0216     dialog.setWindowTitle(i18n("Backup Baskets"));
0217     dialog.setLabelText(i18n("Backing up baskets. Please wait..."));
0218     dialog.setModal(true);
0219     dialog.setCancelButton(nullptr);
0220     dialog.setAutoClose(true);
0221 
0222     dialog.setRange(0, 0 /*Busy/Undefined*/);
0223     dialog.setValue(0);
0224     dialog.show();
0225 
0226     /* If needed, uncomment this and call similar code in other places below
0227     QProgressBar* progress = new QProgressBar(dialog);
0228     progress->setTextVisible(false);
0229     dialog.setBar(progress);*/
0230 
0231     BackupThread thread(destination, Global::savesFolder());
0232     thread.start();
0233     while (thread.isRunning()) {
0234         dialog.setValue(dialog.value() + 1); // Or else, the animation is not played!
0235         qApp->processEvents();
0236         usleep(300); // Not too long because if the backup process is finished, we wait for nothing
0237     }
0238 
0239     Settings::setLastBackup(QDate::currentDate());
0240     Settings::saveConfig();
0241     populateLastBackup();
0242 }
0243 
0244 void BackupDialog::restore()
0245 {
0246     // Get last backup folder:
0247     KConfig *config = KSharedConfig::openConfig().data();
0248     KConfigGroup configGroup(config, "Backups");
0249     QString folder = configGroup.readEntry("lastFolder", QDir::homePath()) + '/';
0250 
0251     // Ask a file name to the user:
0252     QString filter = "*.tar.gz|" + i18n("Tar Archives Compressed by Gzip") + "\n*|" + i18n("All Files");
0253     QString path = QFileDialog::getOpenFileName(this, i18n("Open Basket Archive"), folder, filter);
0254     if (path.isEmpty()) // User has canceled
0255         return;
0256 
0257     // Before replacing the basket data folder with the backup content, we safely backup the current baskets to the home folder.
0258     // So if the backup is corrupted or something goes wrong while restoring (power cut...) the user will be able to restore the old working data:
0259     QString safetyPath = Backup::newSafetyFolder();
0260     FormatImporter copier;
0261     copier.moveFolder(Global::savesFolder(), safetyPath);
0262 
0263     // Add the README file for user to cancel a bad restoration:
0264     QString readmePath = safetyPath + i18n("README.txt");
0265     QFile file(readmePath);
0266     if (file.open(QIODevice::WriteOnly)) {
0267         QTextStream stream(&file);
0268         stream << i18n("This is a safety copy of your baskets like they were before you started to restore the backup %1.", QUrl::fromLocalFile(path).fileName()) + "\n\n"
0269                << i18n("If the restoration was a success and you restored what you wanted to restore, you can remove this folder.") + "\n\n"
0270                << i18n("If something went wrong during the restoration process, you can re-use this folder to store your baskets and nothing will be lost.") + "\n\n"
0271                << i18n("Choose \"Basket\" -> \"Backup & Restore...\" -> \"Use Another Existing Folder...\" and select that folder.") + '\n';
0272         file.close();
0273     }
0274 
0275     QString message = "<p><nobr>" + i18n("Restoring <b>%1</b>. Please wait...", QUrl::fromLocalFile(path).fileName()) + "</nobr></p><p>" + i18n("If something goes wrong during the restoration process, read the file <b>%1</b>.", readmePath);
0276 
0277     QProgressDialog *dialog = new QProgressDialog();
0278     dialog->setWindowTitle(i18n("Restore Baskets"));
0279     dialog->setLabelText(message);
0280     dialog->setModal(/*modal=*/true);
0281     dialog->setCancelButton(nullptr);
0282     dialog->setAutoClose(true);
0283 
0284     dialog->setRange(0, 0 /*Busy/Undefined*/);
0285     dialog->setValue(0);
0286     dialog->show();
0287 
0288     // Uncompress:
0289     RestoreThread thread(path, Global::savesFolder());
0290     thread.start();
0291     while (thread.isRunning()) {
0292         dialog->setValue(dialog->value() + 1); // Or else, the animation is not played!
0293         qApp->processEvents();
0294         usleep(300); // Not too long because if the restore process is finished, we wait for nothing
0295     }
0296 
0297     dialog->hide();   // The restore is finished, do not continue to show it while telling the user the application is going to be restarted
0298     delete dialog;    // If we only hidden it, it reappeared just after having restored a small backup... Very strange.
0299     dialog = nullptr; // This was annoying since it is modal and the "BasKet Note Pads is going to be restarted" message was not reachable.
0300     // qApp->processEvents();
0301 
0302     // Check for errors:
0303     if (!thread.success()) {
0304         // Restore the old baskets:
0305         QDir dir;
0306         dir.remove(readmePath);
0307         copier.moveFolder(safetyPath, Global::savesFolder());
0308         // Tell the user:
0309         KMessageBox::error(nullptr, i18n("This archive is either not a backup of baskets or is corrupted. It cannot be imported. Your old baskets have been preserved instead."), i18n("Restore Error"));
0310         return;
0311     }
0312 
0313     // Note: The safety backup is not removed now because the code can has been wrong, somehow, or the user perhaps restored an older backup by error...
0314     //       The restore process will not be called very often (it is possible it will only be called once or twice around the world during the next years).
0315     //       So it is rare enough to force the user to remove the safety folder, but keep him in control and let him safely recover from restoration errors.
0316 
0317     Backup::setFolderAndRestart(Global::savesFolder() /*No change*/, i18n("Your backup has been successfully restored to <b>%1</b>. %2 is going to be restarted to take this change into account."));
0318 }
0319 
0320 /** class Backup: */
0321 
0322 QString Backup::binaryPath;
0323 
0324 void Backup::figureOutBinaryPath(const char *argv0, QApplication &app)
0325 {
0326     /*
0327        The application can be launched by two ways:
0328        - Globally (app.applicationFilePath() is good)
0329        - In KDevelop or with an absolute path (app.applicationFilePath() is wrong)
0330        This function is called at the very start of main() so that the current directory has not been changed yet.
0331 
0332        Command line (argv[0])   QDir(argv[0]).canonicalPath()                   app.applicationFilePath()
0333        ======================   =============================================   =========================
0334        "basket"                 ""                                              "/opt/kde3/bin/basket"
0335        "./src/.libs/basket"     "/home/seb/prog/basket/debug/src/.lib/basket"   "/opt/kde3/bin/basket"
0336     */
0337 
0338     binaryPath = QDir(argv0).canonicalPath();
0339     if (binaryPath.isEmpty())
0340         binaryPath = app.applicationFilePath();
0341 }
0342 
0343 void Backup::setFolderAndRestart(const QString &folder, const QString &message)
0344 {
0345     // Set the folder:
0346     Settings::setDataFolder(folder);
0347     Settings::saveConfig();
0348 
0349     // Reassure the user that the application main window disappearance is not a crash, but a normal restart.
0350     // This is important for users to trust the application in such a critical phase and understands what's happening:
0351     KMessageBox::information(nullptr, "<qt>" + message.arg((folder.endsWith('/') ? folder.left(folder.length() - 1) : folder), QGuiApplication::applicationDisplayName()), i18n("Restart"));
0352 
0353     // Restart the application:
0354     auto *job = new KIO::CommandLauncherJob(binaryPath);
0355     job->setExecutable(QCoreApplication::applicationName());
0356     job->setIcon(QCoreApplication::applicationName());
0357     job->start();
0358 
0359     exit(0);
0360 }
0361 
0362 QString Backup::newSafetyFolder()
0363 {
0364     QDir dir;
0365     QString fullPath;
0366 
0367     fullPath = QDir::homePath() + '/' + i18nc("Safety folder name before restoring a basket data archive", "Baskets Before Restoration") + '/';
0368     if (!dir.exists(fullPath))
0369         return fullPath;
0370 
0371     for (int i = 2;; ++i) {
0372         fullPath = QDir::homePath() + '/' + i18nc("Safety folder name before restoring a basket data archive", "Baskets Before Restoration (%1)", i) + '/';
0373         if (!dir.exists(fullPath))
0374             return fullPath;
0375     }
0376 
0377     return QString();
0378 }
0379 
0380 /** class BackupThread: */
0381 
0382 BackupThread::BackupThread(const QString &tarFile, const QString &folderToBackup)
0383     : m_tarFile(tarFile)
0384     , m_folderToBackup(folderToBackup)
0385 {
0386 }
0387 
0388 void BackupThread::run()
0389 {
0390     KTar tar(m_tarFile, "application/x-gzip");
0391     tar.open(QIODevice::WriteOnly);
0392     tar.addLocalDirectory(m_folderToBackup, backupMagicFolder);
0393     // KArchive does not add hidden files. Basket description files (".basket") are hidden, we add them manually:
0394     QDir dir(m_folderToBackup + "baskets/");
0395     QStringList baskets = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
0396     for (QStringList::Iterator it = baskets.begin(); it != baskets.end(); ++it) {
0397         tar.addLocalFile(m_folderToBackup + "baskets/" + *it + "/.basket", backupMagicFolder + "/baskets/" + *it + "/.basket");
0398     }
0399     // We finished:
0400     tar.close();
0401 }
0402 
0403 /** class RestoreThread: */
0404 
0405 RestoreThread::RestoreThread(const QString &tarFile, const QString &destFolder)
0406     : m_tarFile(tarFile)
0407     , m_destFolder(destFolder)
0408 {
0409 }
0410 
0411 void RestoreThread::run()
0412 {
0413     m_success = false;
0414     KTar tar(m_tarFile, "application/x-gzip");
0415     tar.open(QIODevice::ReadOnly);
0416     if (tar.isOpen()) {
0417         const KArchiveDirectory *directory = tar.directory();
0418         if (directory->entries().contains(backupMagicFolder)) {
0419             const KArchiveEntry *entry = directory->entry(backupMagicFolder);
0420             if (entry->isDirectory()) {
0421                 ((const KArchiveDirectory *)entry)->copyTo(m_destFolder);
0422                 m_success = true;
0423             }
0424         }
0425         tar.close();
0426     }
0427 }