diff options
author | tpearson <tpearson@283d02a7-25f6-0310-bc7c-ecb5cbfe19da> | 2010-01-10 00:18:25 +0000 |
---|---|---|
committer | tpearson <tpearson@283d02a7-25f6-0310-bc7c-ecb5cbfe19da> | 2010-01-10 00:18:25 +0000 |
commit | f21e5792b5084f5d008bf46f6316030c6dfb31e5 (patch) | |
tree | d51583b36aa1672bac78d98a682cdc330df27e4d /src/backup.cpp | |
download | basket-f21e5792b5084f5d008bf46f6316030c6dfb31e5.tar.gz basket-f21e5792b5084f5d008bf46f6316030c6dfb31e5.zip |
Add author-abandoned basket application
git-svn-id: svn://anonsvn.kde.org/home/kde/branches/trinity/applications/basket@1072339 283d02a7-25f6-0310-bc7c-ecb5cbfe19da
Diffstat (limited to 'src/backup.cpp')
-rw-r--r-- | src/backup.cpp | 414 |
1 files changed, 414 insertions, 0 deletions
diff --git a/src/backup.cpp b/src/backup.cpp new file mode 100644 index 0000000..00bc660 --- /dev/null +++ b/src/backup.cpp @@ -0,0 +1,414 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * slaout@linux62.org * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "backup.h" + +#include "global.h" +#include "variouswidgets.h" +#include "settings.h" +#include "tools.h" +#include "formatimporter.h" // To move a folder + +#include <qhbox.h> +#include <qvbox.h> +#include <qlayout.h> +#include <qlabel.h> +#include <qpushbutton.h> +#include <klocale.h> +#include <qdir.h> +#include <kapplication.h> +#include <kaboutdata.h> +#include <qgroupbox.h> +#include <kdirselectdialog.h> +#include <krun.h> +#include <kconfig.h> +#include <ktar.h> +#include <kfiledialog.h> +#include <kprogress.h> +#include <kmessagebox.h> +#include <cstdlib> +#include <unistd.h> // usleep() + +/** + * Backups are wrapped in a .tar.gz, inside that folder name. + * An archive is not a backup or is corrupted if data are not in that folder! + */ +const QString backupMagicFolder = "BasKet-Note-Pads_Backup"; + +/** class BackupDialog: */ + +BackupDialog::BackupDialog(QWidget *parent, const char *name) + : KDialogBase(parent, name, /*modal=*/true, i18n("Backup & Restore"), + KDialogBase::Close, KDialogBase::Close, /*separator=*/false) +{ + QVBox *page = makeVBoxMainWidget(); +// page->setSpacing(spacingHint()); + + QString savesFolder = Global::savesFolder(); + savesFolder = savesFolder.left(savesFolder.length() - 1); // savesFolder ends with "/" + + QGroupBox *folderGroup = new QGroupBox(1, Qt::Horizontal, i18n("Save Folder"), page); + new QLabel("<qt><nobr>" + i18n("Your baskets are currently stored in that folder:<br><b>%1</b>").arg(savesFolder), folderGroup); + QWidget *folderWidget = new QWidget(folderGroup); + QHBoxLayout *folderLayout = new QHBoxLayout(folderWidget, 0, spacingHint()); + QPushButton *moveFolder = new QPushButton(i18n("&Move to Another Folder..."), folderWidget); + QPushButton *useFolder = new QPushButton(i18n("&Use Another Existing Folder..."), folderWidget); + HelpLabel *helpLabel = new HelpLabel(i18n("Why to do that?"), i18n( + "<p>You can move the folder where %1 store your baskets to:</p><ul>" + "<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>" + "<li>Store your baskets on a server to share them between two computers.<br>" + "In this case, mount the shared-folder to the local file system and ask %2 to use that mount point.<br>" + "Warning: you should not run %3 at the same time on both computers, or you risk to loss data while the two applications are desynced.</li>" + "</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>") + .arg(kapp->aboutData()->programName()) + .arg(kapp->aboutData()->programName()) + .arg(kapp->aboutData()->programName()), + folderWidget); + folderLayout->addWidget(moveFolder); + folderLayout->addWidget(useFolder); + folderLayout->addWidget(helpLabel); + folderLayout->addStretch(); + connect( moveFolder, SIGNAL(clicked()), this, SLOT(moveToAnotherFolder()) ); + connect( useFolder, SIGNAL(clicked()), this, SLOT(useAnotherExistingFolder()) ); + + QGroupBox *backupGroup = new QGroupBox(1, Qt::Horizontal, i18n("Backups"), page); + QWidget *backupWidget = new QWidget(backupGroup); + QHBoxLayout *backupLayout = new QHBoxLayout(backupWidget, 0, spacingHint()); + QPushButton *backupButton = new QPushButton(i18n("&Backup..."), backupWidget); + QPushButton *restoreButton = new QPushButton(i18n("&Restore a Backup..."), backupWidget); + m_lastBackup = new QLabel("", backupWidget); + backupLayout->addWidget(backupButton); + backupLayout->addWidget(restoreButton); + backupLayout->addWidget(m_lastBackup); + backupLayout->addStretch(); + connect( backupButton, SIGNAL(clicked()), this, SLOT(backup()) ); + connect( restoreButton, SIGNAL(clicked()), this, SLOT(restore()) ); + + populateLastBackup(); + + (new QWidget(page))->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); +} + +BackupDialog::~BackupDialog() +{ +} + +void BackupDialog::populateLastBackup() +{ + QString lastBackupText = i18n("Last backup: never"); + if (Settings::lastBackup().isValid()) + lastBackupText = i18n("Last backup: %1").arg(Settings::lastBackup().toString(Qt::LocalDate)); + + m_lastBackup->setText(lastBackupText); +} + +void BackupDialog::moveToAnotherFolder() +{ + KURL selectedURL = KDirSelectDialog::selectDirectory( + /*startDir=*/Global::savesFolder(), /*localOnly=*/true, /*parent=*/0, + /*caption=*/i18n("Choose a Folder Where to Move Baskets")); + + if (!selectedURL.isEmpty()) { + QString folder = selectedURL.path(); + QDir dir(folder); + // The folder should not exists, or be empty (because KDirSelectDialog will likely create it anyway): + if (dir.exists()) { + // Get the content of the folder: + QStringList content = dir.entryList(); + if (content.count() > 2) { // "." and ".." + int result = KMessageBox::questionYesNo( + 0, + "<qt>" + i18n("The folder <b>%1</b> is not empty. Do you want to override it?").arg(folder), + i18n("Override Folder?"), + KGuiItem(i18n("&Override"), "filesave") + ); + if (result == KMessageBox::No) + return; + } + Tools::deleteRecursively(folder); + } + FormatImporter copier; + copier.moveFolder(Global::savesFolder(), folder); + Backup::setFolderAndRestart(folder, i18n("Your baskets have been successfuly moved to <b>%1</b>. %2 is going to be restarted to take this change into account.")); + } +} + +void BackupDialog::useAnotherExistingFolder() +{ + KURL selectedURL = KDirSelectDialog::selectDirectory( + /*startDir=*/Global::savesFolder(), /*localOnly=*/true, /*parent=*/0, + /*caption=*/i18n("Choose an Existing Folder to Store Baskets")); + + if (!selectedURL.isEmpty()) { + Backup::setFolderAndRestart(selectedURL.path(), i18n("Your basket save folder has been successfuly changed to <b>%1</b>. %2 is going to be restarted to take this change into account.")); + } +} + +void BackupDialog::backup() +{ + QDir dir; + + // Compute a default file name & path (eg. "Baskets_2007-01-31.tar.gz"): + KConfig *config = KGlobal::config(); + config->setGroup("Backups"); + QString folder = config->readEntry("lastFolder", QDir::homeDirPath()) + "/"; + QString fileName = i18n("Backup filename (without extension), %1 is the date", "Baskets_%1") + .arg(QDate::currentDate().toString(Qt::ISODate)); + QString url = folder + fileName; + + // Ask a file name & path to the user: + QString filter = "*.tar.gz|" + i18n("Tar Archives Compressed by Gzip") + "\n*|" + i18n("All Files"); + QString destination = url; + for (bool askAgain = true; askAgain; ) { + // Ask: + destination = KFileDialog::getSaveFileName(destination, filter, 0, i18n("Backup Baskets")); + // User canceled? + if (destination.isEmpty()) + return; + // File already existing? Ask for overriding: + if (dir.exists(destination)) { + int result = KMessageBox::questionYesNoCancel( + 0, + "<qt>" + i18n("The file <b>%1</b> already exists. Do you really want to override it?") + .arg(KURL(destination).fileName()), + i18n("Override File?"), + KGuiItem(i18n("&Override"), "filesave") + ); + if (result == KMessageBox::Cancel) + return; + else if (result == KMessageBox::Yes) + askAgain = false; + } else + askAgain = false; + } + + KProgressDialog dialog(0, 0, i18n("Backup Baskets"), i18n("Backing up baskets. Please wait..."), /*modal=*/true); + dialog.setAllowCancel(false); + dialog.setAutoClose(true); + dialog.show(); + KProgress *progress = dialog.progressBar(); + progress->setTotalSteps(0/*Busy/Undefined*/); + progress->setProgress(0); + progress->setPercentageVisible(false); + + BackupThread thread(destination, Global::savesFolder()); + thread.start(); + while (thread.running()) { + progress->advance(1); // Or else, the animation is not played! + kapp->processEvents(); + usleep(300); // Not too long because if the backup process is finished, we wait for nothing + } + + Settings::setLastBackup(QDate::currentDate()); + Settings::saveConfig(); + populateLastBackup(); +} + +void BackupDialog::restore() +{ + // Get last backup folder: + KConfig *config = KGlobal::config(); + config->setGroup("Backups"); + QString folder = config->readEntry("lastFolder", QDir::homeDirPath()) + "/"; + + // Ask a file name to the user: + QString filter = "*.tar.gz|" + i18n("Tar Archives Compressed by Gzip") + "\n*|" + i18n("All Files"); + QString path = KFileDialog::getOpenFileName(folder, filter, this, i18n("Open Basket Archive")); + if (path.isEmpty()) // User has canceled + return; + + // Before replacing the basket data folder with the backup content, we safely backup the current baskets to the home folder. + // 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: + QString safetyPath = Backup::newSafetyFolder(); + FormatImporter copier; + copier.moveFolder(Global::savesFolder(), safetyPath); + + // Add the README file for user to cancel a bad restoration: + QString readmePath = safetyPath + i18n("README.txt"); + QFile file(readmePath); + if (file.open(IO_WriteOnly)) { + QTextStream stream(&file); + stream << i18n("This is a safety copy of your baskets like they were before you started to restore the backup %1.").arg(KURL(path).fileName()) + "\n\n" + << i18n("If the restoration was a success and you restored what you wanted to restore, you can remove this folder.") + "\n\n" + << 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" + << i18n("Choose \"Basket\" -> \"Backup & Restore...\" -> \"Use Another Existing Folder...\" and select that folder.") + "\n"; + file.close(); + } + + QString message = + "<p><nobr>" + i18n("Restoring <b>%1</b>. Please wait...").arg(KURL(path).fileName()) + "</nobr></p><p>" + + i18n("If something goes wrong during the restoration process, read the file <b>%1</b>.").arg(readmePath); + + KProgressDialog *dialog = new KProgressDialog(0, 0, i18n("Restore Baskets"), message, /*modal=*/true); + dialog->setAllowCancel(false); + dialog->setAutoClose(true); + dialog->show(); + KProgress *progress = dialog->progressBar(); + progress->setTotalSteps(0/*Busy/Undefined*/); + progress->setProgress(0); + progress->setPercentageVisible(false); + + // Uncompress: + RestoreThread thread(path, Global::savesFolder()); + thread.start(); + while (thread.running()) { + progress->advance(1); // Or else, the animation is not played! + kapp->processEvents(); + usleep(300); // Not too long because if the restore process is finished, we wait for nothing + } + + dialog->hide(); // The restore is finished, do not continue to show it while telling the user the application is going to be restarted + delete dialog; // If we only hidden it, it reappeared just after having restored a small backup... Very strange. + dialog = 0; // This was annoying since it is modal and the "BasKet Note Pads is going to be restarted" message was not reachable. + //kapp->processEvents(); + + // Check for errors: + if (!thread.success()) { + // Restore the old baskets: + QDir dir; + dir.remove(readmePath); + copier.moveFolder(safetyPath, Global::savesFolder()); + // Tell the user: + KMessageBox::error(0, 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")); + return; + } + + // Note: The safety backup is not removed now because the code can has been wrong, somehow, or the user perhapse restored an older backup by error... + // The restore process will not be called very often (it is possible it will only be called once or twice arround the world during the next years). + // 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. + + Backup::setFolderAndRestart(Global::savesFolder()/*No change*/, i18n("Your backup has been successfuly restored to <b>%1</b>. %2 is going to be restarted to take this change into account.")); +} + +/** class Backup: */ + +QString Backup::binaryPath = ""; + +#include <iostream> + +void Backup::figureOutBinaryPath(const char *argv0, QApplication &app) +{ + /* + The application can be launched by two ways: + - Globaly (app.applicationFilePath() is good) + - In KDevelop or with an absolute path (app.applicationFilePath() is wrong) + This function is called at the very start of main() so that the current directory has not been changed yet. + + Command line (argv[0]) QDir(argv[0]).canonicalPath() app.applicationFilePath() + ====================== ============================================= ========================= + "basket" "" "/opt/kde3/bin/basket" + "./src/.libs/basket" "/home/seb/prog/basket/debug/src/.lib/basket" "/opt/kde3/bin/basket" + */ + + binaryPath = QDir(argv0).canonicalPath(); + if (binaryPath.isEmpty()) + binaryPath = app.applicationFilePath(); +} + +void Backup::setFolderAndRestart(const QString &folder, const QString &message) +{ + // Set the folder: + Settings::setDataFolder(folder); + Settings::saveConfig(); + + // Rassure the user that the application main window disapearition is not a crash, but a normal restart. + // This is important for users to trust the application in such a critical phase and understands what's happening: + KMessageBox::information( + 0, + "<qt>" + message.arg( + (folder.endsWith("/") ? folder.left(folder.length() - 1) : folder), + kapp->aboutData()->programName()), + i18n("Restart") + ); + + // Restart the application: + KRun::runCommand(binaryPath, kapp->aboutData()->programName(), kapp->iconName()); + exit(0); +} + +QString Backup::newSafetyFolder() +{ + QDir dir; + QString fullPath; + + fullPath = QDir::homeDirPath() + "/" + i18n("Safety folder name before restoring a basket data archive", "Baskets Before Restoration") + "/"; + if (!dir.exists(fullPath)) + return fullPath; + + for (int i = 2; ; ++i) { + fullPath = QDir::homeDirPath() + "/" + i18n("Safety folder name before restoring a basket data archive", "Baskets Before Restoration (%1)").arg(i) + "/"; + if (!dir.exists(fullPath)) + return fullPath; + } + + return ""; +} + +/** class BackupThread: */ + +BackupThread::BackupThread(const QString &tarFile, const QString &folderToBackup) + : m_tarFile(tarFile), m_folderToBackup(folderToBackup) +{ +} + +void BackupThread::run() +{ + KTar tar(m_tarFile, "application/x-gzip"); + tar.open(IO_WriteOnly); + tar.addLocalDirectory(m_folderToBackup, backupMagicFolder); + // KArchive does not add hidden files. Basket description files (".basket") are hidden, we add them manually: + QDir dir(m_folderToBackup + "baskets/"); + QStringList baskets = dir.entryList(QDir::Dirs); + for (QStringList::Iterator it = baskets.begin(); it != baskets.end(); ++it) { + tar.addLocalFile( + m_folderToBackup + "baskets/" + *it + "/.basket", + backupMagicFolder + "/baskets/" + *it + "/.basket" + ); + } + // We finished: + tar.close(); +} + +/** class RestoreThread: */ + +RestoreThread::RestoreThread(const QString &tarFile, const QString &destFolder) + : m_tarFile(tarFile), m_destFolder(destFolder) +{ +} + +void RestoreThread::run() +{ + m_success = false; + KTar tar(m_tarFile, "application/x-gzip"); + tar.open(IO_ReadOnly); + if (tar.isOpened()) { + const KArchiveDirectory *directory = tar.directory(); + if (directory->entries().contains(backupMagicFolder)) { + const KArchiveEntry *entry = directory->entry(backupMagicFolder); + if (entry->isDirectory()) { + ((const KArchiveDirectory*) entry)->copyTo(m_destFolder); + m_success = true; + } + } + tar.close(); + } +} + +#include "backup.moc" |