--- /dev/null
+/*
+ * 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());
+}