1st attempt at an initial import.
[qwerkisync] / EventLogBackupManager.cpp
diff --git a/EventLogBackupManager.cpp b/EventLogBackupManager.cpp
new file mode 100644 (file)
index 0000000..271b4cc
--- /dev/null
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2011, Jamie Thompson
+ *
+ * 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 3 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, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+#include "EventLogBackupManager.h"
+#include "EventPreventer.h"
+
+#include <QtDebug>
+
+#include <QDateTime>
+#include <QDir>
+#include <QFile>
+#include <QStringList>
+#include <QtAlgorithms>
+
+#include <stdexcept>
+
+EventLogBackupManager::EventLogBackupManager(const Settings & currentSettings) :
+       m_kCurrentSettings(currentSettings)
+{
+       setBackupDirectoryPath("/home/user/MyDocs/backups/");
+       setDataDirectoryPath("/home/user/.rtcom-eventlogger/");
+       setCurrentBackupName(QString::number(QDateTime::currentDateTimeUtc().toTime_t()) + ".qsbackup/");
+       setMaxNumberOfBackups(3);
+       setLockFilename(".inuse");
+}
+
+EventLogBackupManager::~EventLogBackupManager()
+{
+}
+
+void copyFileInfoListRecusively(const QFileInfoList &sourceItems, const QString &sourcePath, const QString &destinationPath)
+{
+       foreach(QFileInfo entry, sourceItems)
+       {
+               QString entryStubFilePath(entry.absoluteFilePath().replace(QRegExp("^" + sourcePath), ""));
+               if(entry.isDir())
+               {
+                       if(!QDir().mkpath(destinationPath + entryStubFilePath))
+                               throw std::runtime_error(QString("Unable to make the directory: %1%2").arg(destinationPath).arg(entryStubFilePath).toLocal8Bit().constData());
+
+                       copyFileInfoListRecusively(
+                               QDir(entry.absoluteFilePath()).entryInfoList(
+                                       QDir::AllEntries | QDir::NoDotAndDotDot,
+                                       QDir::DirsFirst),
+                               sourcePath,
+                               destinationPath);
+               }
+               else
+                       if(!QFile(entry.absoluteFilePath()).copy(destinationPath + entryStubFilePath))
+                               throw std::runtime_error(QString("Unable to copy the file '%1'' to '%2%3'").arg(entry.absoluteFilePath()).arg(destinationPath).arg(entryStubFilePath).toLocal8Bit().constData());
+       }
+}
+
+void EventLogBackupManager::CreateBackup()
+{
+       PurgeOldBackups();
+
+       // Make the new directory
+       if(QDir().mkpath(CurrentBackupPath()))
+       {
+               try
+               {
+                       // Copy the data to it
+                       copyFileInfoListRecusively(
+                               QDir(DataDirectoryPath()).entryInfoList(
+                                       QStringList() << "*.db*" << "attachments" << "plugins",
+                                       QDir::AllEntries | QDir::NoDotAndDotDot,
+                                       QDir::DirsFirst),
+                               DataDirectoryPath(),
+                               CurrentBackupPath()
+                       );
+
+                       LockCurrentBackup();
+               }
+               catch(const std::runtime_error &exception)
+               {
+                       RemoveDirRecusively(CurrentBackupPath());
+               }
+       }
+       else
+               throw std::runtime_error(QString("Unable to create backup directory '%1'").arg(CurrentBackupPath()).toLocal8Bit().constData());
+}
+
+void EventLogBackupManager::RestoreBackup(const QString &backupPath)
+{
+       qDebug() << "Restoring backup: " << backupPath;
+
+       // Check backup is valid
+       EnsureBackupValid(backupPath);
+
+       // Remove old working-copy backups
+       {
+               RemoveDirRecusively(QFileInfo(DataDirectoryPath() + "/attachments.qsrestore"));
+               RemoveDirRecusively(QFileInfo(DataDirectoryPath() + "/plugins.qsrestore"));
+               foreach(QFileInfo entry, QDir(DataDirectoryPath()).entryInfoList(QStringList("*.db*.qsrestore")))
+                       QFile(entry.absoluteFilePath()).remove();
+       }
+
+       // Disable new events and try restoring the content
+       EventPreventer noEventsPlease(CurrentSettings());
+       noEventsPlease.DisableAccounts();
+       try
+       {
+               // Move the attachments out of the way and copy in from the backup
+               if(!QDir().rename(DataDirectoryPath() + "/attachments", DataDirectoryPath() + "/attachments.qsrestore"))
+                       throw std::runtime_error("");
+               copyFileInfoListRecusively(QDir(backupPath).entryInfoList(QStringList("attachments")), backupPath, DataDirectoryPath());
+
+               // Move the plugins out of the way and copy in from the backup
+               if(!QDir().rename(DataDirectoryPath() + "/plugins", DataDirectoryPath() + "/plugins.qsrestore"))
+                       throw std::runtime_error("");
+               copyFileInfoListRecusively(QDir(backupPath).entryInfoList(QStringList("plugins")), backupPath, DataDirectoryPath());
+
+               // Move the database files out of the way and copy in from the backup
+               foreach(QFileInfo entry, QDir(DataDirectoryPath()).entryInfoList(QStringList("*.db*")))
+                       QFile(entry.absoluteFilePath()).copy(DataDirectoryPath() + entry.fileName() + ".qsrestore");
+               foreach(QFileInfo entry, QDir(backupPath).entryInfoList(QStringList("*.db*")))
+                       QFile(entry.absoluteFilePath()).copy(DataDirectoryPath() + entry.fileName());
+
+               // Now all of the backup components have been restored, we can reenable the accounts safely
+               noEventsPlease.RestoreAccounts();
+
+               // ...and we can remove the working-copy backups
+               foreach(QFileInfo entry, QDir(DataDirectoryPath()).entryInfoList(QStringList("*.db*.qsrestore")))
+                       QFile(entry.absoluteFilePath()).remove();
+               RemoveDirRecusively(QFileInfo(DataDirectoryPath() + "/plugins.qsrestore"));
+               RemoveDirRecusively(QFileInfo(DataDirectoryPath() + "/attachments.qsrestore"));
+       }
+       catch(const std::runtime_error &exception)
+       {
+               // Remove the partially-restored data
+               foreach(QFileInfo entry, QDir(DataDirectoryPath()).entryInfoList(QStringList("*.db*.qsrestore")))
+                       QFile(entry.absoluteFilePath().remove(".qsrestore")).remove();
+               RemoveDirRecusively(QFileInfo(DataDirectoryPath() + "/plugins"));
+               RemoveDirRecusively(QFileInfo(DataDirectoryPath() + "/attachments"));
+
+               // Revert attachments
+               if(!QDir().rename(DataDirectoryPath() + "/attachments.qsrestore", DataDirectoryPath() + "/attachments"))
+                       throw std::runtime_error("");
+
+               // Revert plugins
+               if(!QDir().rename(DataDirectoryPath() + "/plugins.qsrestore", DataDirectoryPath() + "/plugins"))
+                       throw std::runtime_error("");
+
+               // Revert databases
+               foreach(QFileInfo entry, QDir(DataDirectoryPath()).entryInfoList(QStringList("*.db*.qsrestore")))
+                       QFile(entry.absoluteFilePath()).copy(DataDirectoryPath() + entry.fileName().remove(".qsrestore"));
+
+               // Now all of the working-copy components have been restored, we can reenable the accounts safely
+               noEventsPlease.RestoreAccounts();
+
+               // ..but the restoe still failed, so tell the caller about it.
+               throw;
+       }
+}
+
+void EventLogBackupManager::LockCurrentBackup()
+{
+       LockBackup(CurrentBackupPath());
+}
+
+void EventLogBackupManager::UnlockCurrentBackup()
+{
+       UnlockBackup(CurrentBackupPath());
+}
+
+void EventLogBackupManager::LockBackup(const QString &backupPath)
+{
+       qDebug() << "Locking backup: " << backupPath;
+
+       // Mark the backup as "in use" by touching a lockfile.
+       QFile lockfile(backupPath + LockFilename());
+       lockfile.open(QIODevice::WriteOnly);
+}
+
+void EventLogBackupManager::UnlockBackup(const QString &backupPath)
+{
+       qDebug() << "Unlocking backup: " << backupPath;
+
+       QFile lockfile(QString("%1/%2").arg(backupPath).arg(LockFilename()));
+       lockfile.remove();
+}
+
+// Ideally would be local to PurgeOldBackups, but template arguments have to
+// refer to types with external linkage. Roll on C++0x!
+class OrderByTimestamp
+{
+public:
+       inline bool operator()(const QFileInfo &a, QFileInfo &b) const
+       {
+               return b.created() < a.created();
+       }
+};
+
+void EventLogBackupManager::PurgeOldBackups()
+{
+       // Enumerate backups directory
+       QFileInfoList existingBackups(CurrentBackups(false));
+
+       // If more than maximum number of backups found, delete the oldest
+       if((uint)existingBackups.count() > MaxNumberOfBackups() - 1)
+       {
+               // This is important, so explicitly make sure the list is in the correct order
+               qSort(existingBackups.begin(), existingBackups.end(), OrderByTimestamp());
+
+               for(int i(0); i < existingBackups.count(); ++i)
+               {
+                       if(i < 2)
+                               qDebug() << existingBackups.value(i).absoluteFilePath();
+                       else
+                               RemoveDirRecusively(existingBackups.value(i));
+               }
+       }
+}
+
+const QFileInfoList EventLogBackupManager::CurrentBackups(bool lockedOnly)
+{
+       QFileInfoList existingBackups;
+       QDir backupDirectory(BackupDirectoryPath());
+       foreach(QFileInfo entry, backupDirectory.entryInfoList(QStringList("*.qsbackup"), QDir::AllEntries | QDir::NoDotAndDotDot, QDir::Name | QDir::Reversed))
+       {
+               // If we only want locked backups, then skip those without the lock file present...
+               if(lockedOnly && QDir(entry.absoluteFilePath()).entryInfoList(QStringList(LockFilename()), QDir::Hidden).count() == 0)
+               {
+                       qDebug() << "Ignoring unlocked backup: " << entry.absoluteFilePath();
+                       continue;
+               }
+
+               qDebug() << "Locked backup found: " << entry.absoluteFilePath();
+               existingBackups.append(QFileInfo(entry));
+       }
+
+       return existingBackups;
+}
+
+void EventLogBackupManager::RemoveDirRecusively(const QFileInfo &dirInfo)
+{
+       foreach(QFileInfo entry,
+                       QDir(dirInfo.absoluteFilePath()).entryInfoList(
+                               QDir::AllEntries | QDir::NoDotAndDotDot,
+                               QDir::DirsFirst))
+       {
+               if(entry.isDir())
+                       RemoveDirRecusively(entry);
+               else
+                       QDir().remove(entry.absoluteFilePath());
+       }
+
+       // Dir will be empty as we've removed all dirs and files...
+       QDir().rmdir(dirInfo.absoluteFilePath());
+}
+
+void EventLogBackupManager::EnsureBackupValid(const QString &backupPath)
+{
+       QString shortBackupPath(backupPath);
+       shortBackupPath.remove(BackupDirectoryPath());
+
+       bool oldDBPresent(QFile(backupPath + "/el.db").exists() && QFile(backupPath + "/el.db-journal").exists());
+       bool v1DBPresent(QFile(backupPath + "/el-v1.db").exists() && QFile(backupPath + "/el-v1.db-journal").exists());
+       if( !(oldDBPresent || v1DBPresent ) )
+               throw std::runtime_error(QString("The backup '%1' is missing the main event logger database.").arg(shortBackupPath).toLocal8Bit().constData());
+
+       if(!QDir(backupPath + "/attachments").exists())
+               throw std::runtime_error(QString("The backup '%1' is missing the attachments directory.").arg(shortBackupPath).toLocal8Bit().constData());
+
+       if(!QDir(backupPath + "/plugins").exists())
+               throw std::runtime_error(QString("The backup '%1' is missing the plugins directory.").arg(shortBackupPath).toLocal8Bit().constData());
+}