File indexing completed on 2024-04-21 16:31:44

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     const QString currentSavesFolder = Global::savesFolder();
0149     const QUrl selectedURL = QFileDialog::getExistingDirectoryUrl(this,
0150                                                                   i18n("Choose a Folder Where to Move Baskets"),
0151                                                                   QUrl::fromLocalFile(currentSavesFolder));
0152 
0153     if (!selectedURL.isEmpty()) {
0154         QString folder = selectedURL.path();
0155         QDir dir(folder);
0156         // The folder should not exists, or be empty (because KDirSelectDialog will likely create it anyway):
0157         if (dir.exists()) {
0158             // Get the content of the folder:
0159             QStringList content = dir.entryList();
0160             if (content.count() > 2) { // "." and ".."
0161                 int result = KMessageBox::warningContinueCancel(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"));
0162                 if (result == KMessageBox::Cancel)
0163                     return;
0164             }
0165             Tools::deleteRecursively(folder);
0166         }
0167         FormatImporter copier;
0168         copier.moveFolder(currentSavesFolder, folder);
0169         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."));
0170     }
0171 }
0172 
0173 void BackupDialog::useAnotherExistingFolder()
0174 {
0175     const QString currentSavesFolder = Global::savesFolder();
0176     const QUrl selectedURL = QFileDialog::getExistingDirectoryUrl(this,
0177                                                                   i18n("Choose a Folder Where to Move Baskets"),
0178                                                                   QUrl::fromLocalFile(currentSavesFolder));
0179 
0180     if (!selectedURL.isEmpty()) {
0181         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."));
0182     }
0183 }
0184 
0185 void BackupDialog::backup()
0186 {
0187     QDir dir;
0188 
0189     // Compute a default file name & path (eg. "Baskets_2007-01-31.tar.gz"):
0190     KConfig *config = KSharedConfig::openConfig().data();
0191     KConfigGroup configGroup(config, "Backups");
0192     QString folder = configGroup.readEntry("lastFolder", QDir::homePath()) + '/';
0193     QString fileName = i18nc("Backup filename (without extension), %1 is the date", "Baskets_%1", QDate::currentDate().toString(Qt::ISODate));
0194     QString url = folder + fileName;
0195 
0196     // Ask a file name & path to the user:
0197     QString filter = "*.tar.gz|" + i18n("Tar Archives Compressed by Gzip") + "\n*|" + i18n("All Files");
0198     const QString destination = QFileDialog::getSaveFileName(nullptr, i18n("Backup Baskets"), url, filter);
0199 
0200     // User canceled?
0201     if (destination.isEmpty()) {
0202         return;
0203     }
0204 
0205     QProgressDialog dialog;
0206     dialog.setWindowTitle(i18n("Backup Baskets"));
0207     dialog.setLabelText(i18n("Backing up baskets. Please wait..."));
0208     dialog.setModal(true);
0209     dialog.setCancelButton(nullptr);
0210     dialog.setAutoClose(true);
0211 
0212     dialog.setRange(0, 0 /*Busy/Undefined*/);
0213     dialog.setValue(0);
0214     dialog.show();
0215 
0216     /* If needed, uncomment this and call similar code in other places below
0217     QProgressBar* progress = new QProgressBar(dialog);
0218     progress->setTextVisible(false);
0219     dialog.setBar(progress);*/
0220 
0221     BackupThread thread(destination, Global::savesFolder());
0222     thread.start();
0223     while (thread.isRunning()) {
0224         dialog.setValue(dialog.value() + 1); // Or else, the animation is not played!
0225         qApp->processEvents();
0226         usleep(300); // Not too long because if the backup process is finished, we wait for nothing
0227     }
0228 
0229     Settings::setLastBackup(QDate::currentDate());
0230     Settings::saveConfig();
0231     populateLastBackup();
0232 }
0233 
0234 void BackupDialog::restore()
0235 {
0236     // Get last backup folder:
0237     KConfig *config = KSharedConfig::openConfig().data();
0238     KConfigGroup configGroup(config, "Backups");
0239     QString folder = configGroup.readEntry("lastFolder", QDir::homePath()) + '/';
0240 
0241     // Ask a file name to the user:
0242     QString filter = "*.tar.gz|" + i18n("Tar Archives Compressed by Gzip") + "\n*|" + i18n("All Files");
0243     QString path = QFileDialog::getOpenFileName(this, i18n("Open Basket Archive"), folder, filter);
0244     if (path.isEmpty()) // User has canceled
0245         return;
0246 
0247     // Before replacing the basket data folder with the backup content, we safely backup the current baskets to the home folder.
0248     // 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:
0249     QString safetyPath = Backup::newSafetyFolder();
0250     FormatImporter copier;
0251     copier.moveFolder(Global::savesFolder(), safetyPath);
0252 
0253     // Add the README file for user to cancel a bad restoration:
0254     QString readmePath = safetyPath + i18n("README.txt");
0255     QFile file(readmePath);
0256     if (file.open(QIODevice::WriteOnly)) {
0257         QTextStream stream(&file);
0258         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"
0259                << i18n("If the restoration was a success and you restored what you wanted to restore, you can remove this folder.") + "\n\n"
0260                << 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"
0261                << i18n("Choose \"Basket\" -> \"Backup & Restore...\" -> \"Use Another Existing Folder...\" and select that folder.") + '\n';
0262         file.close();
0263     }
0264 
0265     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);
0266 
0267     QProgressDialog *dialog = new QProgressDialog();
0268     dialog->setWindowTitle(i18n("Restore Baskets"));
0269     dialog->setLabelText(message);
0270     dialog->setModal(/*modal=*/true);
0271     dialog->setCancelButton(nullptr);
0272     dialog->setAutoClose(true);
0273 
0274     dialog->setRange(0, 0 /*Busy/Undefined*/);
0275     dialog->setValue(0);
0276     dialog->show();
0277 
0278     // Uncompress:
0279     RestoreThread thread(path, Global::savesFolder());
0280     thread.start();
0281     while (thread.isRunning()) {
0282         dialog->setValue(dialog->value() + 1); // Or else, the animation is not played!
0283         qApp->processEvents();
0284         usleep(300); // Not too long because if the restore process is finished, we wait for nothing
0285     }
0286 
0287     dialog->hide();   // The restore is finished, do not continue to show it while telling the user the application is going to be restarted
0288     delete dialog;    // If we only hidden it, it reappeared just after having restored a small backup... Very strange.
0289     dialog = nullptr; // This was annoying since it is modal and the "BasKet Note Pads is going to be restarted" message was not reachable.
0290     // qApp->processEvents();
0291 
0292     // Check for errors:
0293     if (!thread.success()) {
0294         // Restore the old baskets:
0295         QDir dir;
0296         dir.remove(readmePath);
0297         copier.moveFolder(safetyPath, Global::savesFolder());
0298         // Tell the user:
0299         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"));
0300         return;
0301     }
0302 
0303     // 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...
0304     //       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).
0305     //       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.
0306 
0307     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."));
0308 }
0309 
0310 /** class Backup: */
0311 
0312 QString Backup::binaryPath;
0313 
0314 void Backup::figureOutBinaryPath(const char *argv0, QApplication &app)
0315 {
0316     /*
0317        The application can be launched by two ways:
0318        - Globally (app.applicationFilePath() is good)
0319        - In KDevelop or with an absolute path (app.applicationFilePath() is wrong)
0320        This function is called at the very start of main() so that the current directory has not been changed yet.
0321 
0322        Command line (argv[0])   QDir(argv[0]).canonicalPath()                   app.applicationFilePath()
0323        ======================   =============================================   =========================
0324        "basket"                 ""                                              "/opt/kde3/bin/basket"
0325        "./src/.libs/basket"     "/home/seb/prog/basket/debug/src/.lib/basket"   "/opt/kde3/bin/basket"
0326     */
0327 
0328     binaryPath = QDir(argv0).canonicalPath();
0329     if (binaryPath.isEmpty())
0330         binaryPath = app.applicationFilePath();
0331 }
0332 
0333 void Backup::setFolderAndRestart(const QString &folder, const QString &message)
0334 {
0335     // Set the folder:
0336     Settings::setDataFolder(folder);
0337     Settings::saveConfig();
0338 
0339     // Reassure the user that the application main window disappearance is not a crash, but a normal restart.
0340     // This is important for users to trust the application in such a critical phase and understands what's happening:
0341     KMessageBox::information(nullptr, "<qt>" + message.arg((folder.endsWith('/') ? folder.left(folder.length() - 1) : folder), QGuiApplication::applicationDisplayName()), i18n("Restart"));
0342 
0343     // Restart the application:
0344     auto *job = new KIO::CommandLauncherJob(binaryPath);
0345     job->setExecutable(QCoreApplication::applicationName());
0346     job->setIcon(QCoreApplication::applicationName());
0347     job->start();
0348 
0349     exit(0);
0350 }
0351 
0352 QString Backup::newSafetyFolder()
0353 {
0354     QDir dir;
0355     QString fullPath;
0356 
0357     fullPath = QDir::homePath() + '/' + i18nc("Safety folder name before restoring a basket data archive", "Baskets Before Restoration") + '/';
0358     if (!dir.exists(fullPath))
0359         return fullPath;
0360 
0361     for (int i = 2;; ++i) {
0362         fullPath = QDir::homePath() + '/' + i18nc("Safety folder name before restoring a basket data archive", "Baskets Before Restoration (%1)", i) + '/';
0363         if (!dir.exists(fullPath))
0364             return fullPath;
0365     }
0366 
0367     return QString();
0368 }
0369 
0370 /** class BackupThread: */
0371 
0372 BackupThread::BackupThread(const QString &tarFile, const QString &folderToBackup)
0373     : m_tarFile(tarFile)
0374     , m_folderToBackup(folderToBackup)
0375 {
0376 }
0377 
0378 void BackupThread::run()
0379 {
0380     KTar tar(m_tarFile, "application/x-gzip");
0381     tar.open(QIODevice::WriteOnly);
0382     tar.addLocalDirectory(m_folderToBackup, backupMagicFolder);
0383     // KArchive does not add hidden files. Basket description files (".basket") are hidden, we add them manually:
0384     QDir dir(m_folderToBackup + "baskets/");
0385     QStringList baskets = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
0386     for (QStringList::Iterator it = baskets.begin(); it != baskets.end(); ++it) {
0387         tar.addLocalFile(m_folderToBackup + "baskets/" + *it + "/.basket", backupMagicFolder + "/baskets/" + *it + "/.basket");
0388     }
0389     // We finished:
0390     tar.close();
0391 }
0392 
0393 /** class RestoreThread: */
0394 
0395 RestoreThread::RestoreThread(const QString &tarFile, const QString &destFolder)
0396     : m_tarFile(tarFile)
0397     , m_destFolder(destFolder)
0398 {
0399 }
0400 
0401 void RestoreThread::run()
0402 {
0403     m_success = false;
0404     KTar tar(m_tarFile, "application/x-gzip");
0405     tar.open(QIODevice::ReadOnly);
0406     if (tar.isOpen()) {
0407         const KArchiveDirectory *directory = tar.directory();
0408         if (directory->entries().contains(backupMagicFolder)) {
0409             const KArchiveEntry *entry = directory->entry(backupMagicFolder);
0410             if (entry->isDirectory()) {
0411                 ((const KArchiveDirectory *)entry)->copyTo(m_destFolder);
0412                 m_success = true;
0413             }
0414         }
0415         tar.close();
0416     }
0417 }
0418 
0419 #include "moc_backup.cpp"