File indexing completed on 2024-03-24 05:51:03
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"