From 120a899b105f142504a8176963395f2d162e6d6b Mon Sep 17 00:00:00 2001 From: Luca Carlon Date: Sun, 23 Sep 2018 02:16:05 +0200 Subject: [PATCH] Added support for Restic. --- .gitignore | 4 + CMakeLists.txt | 3 + daemon/CMakeLists.txt | 3 + daemon/planexecutor.cpp | 20 +- daemon/resticjob.cpp | 288 +++++++++++++++ daemon/resticjob.h | 66 ++++ filedigger/CMakeLists.txt | 6 + filedigger/filedigger.cpp | 281 ++++++++++++-- filedigger/filedigger.h | 62 +++- filedigger/fsminer.cpp | 33 ++ filedigger/fsminer.h | 59 +++ filedigger/fsminerbup.cpp | 51 +++ filedigger/fsminerbup.h | 17 + filedigger/fsminerrestic.cpp | 71 ++++ filedigger/fsminerrestic.h | 37 ++ filedigger/main.cpp | 17 +- filedigger/mergedvfs.cpp | 283 +------------- filedigger/mergedvfs.h | 64 ++-- filedigger/mergedvfsbup.cpp | 312 ++++++++++++++++ filedigger/mergedvfsbup.h | 78 ++++ filedigger/mergedvfsmodel.cpp | 8 +- filedigger/mergedvfsmodel.h | 3 + filedigger/mergedvfsrestic.cpp | 115 ++++++ filedigger/mergedvfsrestic.h | 78 ++++ filedigger/restoredialog.cpp | 25 +- filedigger/restoredialog.h | 4 +- filedigger/versionlistdelegate.cpp | 32 +- filedigger/versionlistdelegate.h | 6 +- filedigger/versionlistmodel.cpp | 6 +- filedigger/versionlistmodel.h | 2 +- kcm/CMakeLists.txt | 1 + kcm/backupplanwidget.cpp | 243 +++++++++++- kcm/backupplanwidget.h | 27 +- kcm/kupkcm.cpp | 28 +- kcm/kupkcm.h | 1 + kioslave/vfshelpers.h | 5 + regex | 1 + resticcore/CMakeLists.txt | 21 ++ resticcore/resticforgetswitch.cpp | 30 ++ resticcore/resticforgetswitch.h | 89 +++++ resticcore/restichelper.cpp | 568 +++++++++++++++++++++++++++++ resticcore/restichelper.h | 249 +++++++++++++ settings/backupplan.cpp | 14 + settings/backupplan.h | 18 +- settings/kuputils.cpp | 36 ++ settings/kuputils.h | 2 + 46 files changed, 2979 insertions(+), 388 deletions(-) create mode 100644 .gitignore create mode 100644 daemon/resticjob.cpp create mode 100644 daemon/resticjob.h create mode 100644 filedigger/fsminer.cpp create mode 100644 filedigger/fsminer.h create mode 100644 filedigger/fsminerbup.cpp create mode 100644 filedigger/fsminerbup.h create mode 100644 filedigger/fsminerrestic.cpp create mode 100644 filedigger/fsminerrestic.h create mode 100644 filedigger/mergedvfsbup.cpp create mode 100644 filedigger/mergedvfsbup.h create mode 100644 filedigger/mergedvfsrestic.cpp create mode 100644 filedigger/mergedvfsrestic.h create mode 100644 regex create mode 100644 resticcore/CMakeLists.txt create mode 100644 resticcore/resticforgetswitch.cpp create mode 100644 resticcore/resticforgetswitch.h create mode 100644 resticcore/restichelper.cpp create mode 100644 resticcore/restichelper.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ef165c2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.kdev4 +CMakeLists.txt.user +Kup.kdev4 +build diff --git a/CMakeLists.txt b/CMakeLists.txt index dff8fc5..bf664ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,6 +48,8 @@ Config Init # needed for the kdeinit cmake macro JobWidgets Plasma +Pty +XmlGui ) add_subdirectory(daemon) @@ -57,6 +59,7 @@ add_subdirectory(filedigger) add_subdirectory(kcm) add_subdirectory(kioslave) add_subdirectory(po) +add_subdirectory(resticcore) plasma_install_package(plasmoid org.kde.kupapplet) diff --git a/daemon/CMakeLists.txt b/daemon/CMakeLists.txt index 997cbe8..82d7084 100644 --- a/daemon/CMakeLists.txt +++ b/daemon/CMakeLists.txt @@ -12,6 +12,7 @@ bupjob.cpp bupverificationjob.cpp buprepairjob.cpp rsyncjob.cpp +resticjob.cpp ../settings/backupplan.cpp ../settings/kupsettings.cpp ../settings/kuputils.cpp @@ -26,6 +27,7 @@ ecm_qt_declare_logging_category(kupdaemon_SRCS kf5_add_kdeinit_executable(kup-daemon ${kupdaemon_SRCS}) target_link_libraries(kdeinit_kup-daemon +resticcore Qt5::Core Qt5::DBus Qt5::Gui @@ -39,6 +41,7 @@ KF5::Solid KF5::Notifications KF5::CoreAddons KF5::DBusAddons +KF5::Pty ) ########### install files ############### diff --git a/daemon/planexecutor.cpp b/daemon/planexecutor.cpp index e6b8ed5..4cd8d96 100644 --- a/daemon/planexecutor.cpp +++ b/daemon/planexecutor.cpp @@ -25,6 +25,8 @@ #include "kupdaemon.h" #include "kupdaemon_debug.h" #include "rsyncjob.h" +#include "resticjob.h" +#include "restichelper.h" #include #include @@ -398,16 +400,22 @@ void PlanExecutor::updateAccumulatedUsageTime() { } } +inline void run_filedigger(const QString &repoPath, const QString &type) +{ + QStringList args = QStringList() << QStringLiteral("-t") << type + << repoPath; + KProcess::startDetached(QStringLiteral("kup-filedigger"), args); +} + void PlanExecutor::showBackupFiles() { if(mState == NOT_AVAILABLE) return; - if(mPlan->mBackupType == BackupPlan::BupType) { - QStringList lArgs; - lArgs << QStringLiteral("--title") << mPlan->mDescription; - lArgs << mDestinationPath; - KProcess::startDetached(QStringLiteral("kup-filedigger"), lArgs); + if(mPlan->mBackupType == BackupPlan::BupType) { + run_filedigger(mDestinationPath, QSL("bup")); } else if(mPlan->mBackupType == BackupPlan::RsyncType) { KRun::runUrl(QUrl::fromLocalFile(mDestinationPath), QStringLiteral("inode/directory"), nullptr); + } else if(mPlan->mBackupType == BackupPlan::ResticType) { + run_filedigger(mDestinationPath, QSL("restic")); } } @@ -416,6 +424,8 @@ BackupJob *PlanExecutor::createBackupJob() { return new BupJob(*mPlan, mDestinationPath, mLogFilePath, mKupDaemon); } else if(mPlan->mBackupType == BackupPlan::RsyncType) { return new RsyncJob(*mPlan, mDestinationPath, mLogFilePath, mKupDaemon); + } else if(mPlan->mBackupType == BackupPlan::ResticType) { + return new ResticJob(*mPlan, mDestinationPath, mLogFilePath, mKupDaemon); } qCWarning(KUPDAEMON) << "Invalid backup type in configuration!"; return nullptr; diff --git a/daemon/resticjob.cpp b/daemon/resticjob.cpp new file mode 100644 index 0000000..cfa7428 --- /dev/null +++ b/daemon/resticjob.cpp @@ -0,0 +1,288 @@ +/*************************************************************************** + * Copyright Luca Carlon * + * carlon.luca@gmail.com * + * * + * 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., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#include +#include + +#include +#include + +#include + +#include "resticjob.h" +#include "kupdaemon_debug.h" + +ResticJob::ResticJob(const BackupPlan &pBackupPlan, + const QString &pDestinationPath, + const QString &pLogFilePath, + KupDaemon *pKupDaemon) : + BackupJob(pBackupPlan, pDestinationPath, pLogFilePath, pKupDaemon) + , mProcessing(false) +{ + qputenv("RESTIC_PASSWORD", "kup"); + setCapabilities(KJob::Suspendable | KJob::Killable); +} + +bool ResticJob::doKill() +{ + if (mBackupProcess && mBackupProcess->state() != KPtyProcess::NotRunning) + ::kill(mBackupProcess->pid(), SIGINT); + if (mForgetProcess && mForgetProcess->state() != KPtyProcess::NotRunning) + ::kill(mForgetProcess->pid(), SIGINT); + +#define TIMEOUT 10000 + + if (mBackupProcess && !mBackupProcess->waitForFinished(TIMEOUT)) + return false; + if (mForgetProcess && !mForgetProcess->waitForFinished(TIMEOUT)) + return false; + + return true; +} + +bool ResticJob::doSuspend() +{ + return sendSignal(SIGSTOP); +} + +bool ResticJob::doResume() +{ + return sendSignal(SIGCONT); +} + +void ResticJob::performJob() +{ + if (mProcessing) + return; + + mProcessing = true; + mLogStream << QStringLiteral("Kup is starting restic backup job at ") + << QLocale().toString(QDateTime::currentDateTime()) + << endl << endl; + + // Determine if command exists. + if (!isCommandAvailable()) { + jobFinishedError(ErrorWithoutLog, xi18nc("@info notification", + "The restic program is " + "needed but could not be found, maybe it is not installed?")); + return; + } + + // Determine if repo exists. + if (!mResticHelper.repoExists(mDestinationPath)) { + qCDebug(KUPDAEMON) << "Repo does not exist: init"; + if (!mResticHelper.repoInit(mDestinationPath)) { + jobFinishedError(ErrorWithLog, xi18nc("@info notification", "Backup could not be initialized.")); + return; + } + + // Repo initilized. + } + + if(mBackupPlan.mCheckBackups) { + emit description(this, i18n("Checking backup integrity")); + + QString output; + if (!mResticHelper.repoCheck(mDestinationPath, output)) { + mLogStream << output; + jobFinishedError(ErrorWithLog, xi18nc("@info notification", + "Failed backup integrity check. Your backups could be corrupted! " + "See log file for more details.")); + return; + } + + // Repo is not corrupted. + } + + mBackupProcess.reset(mResticHelper.repoBackup(mDestinationPath, + mBackupPlan.mPathsIncluded, + mBackupPlan.mPathsExcluded)); + if (!mBackupProcess) { + jobFinishedError(ErrorWithLog, xi18nc("@info notification", + "Failed to start backup process. " + "See log file for more details.")); + return; + } + + connect(mBackupProcess.data(), SIGNAL(backupStep(ResticProgressState)), + this, SLOT(onBackupStep(ResticProgressState))); + connect(mBackupProcess.data(), SIGNAL(backupFailed()), + this, SLOT(onBackupFailed())); + connect(mBackupProcess.data(), SIGNAL(backupCompleted()), + this, SLOT(onBackupCompleted())); +} + +void ResticJob::onBackupStep(ResticProgressState state) +{ + if (!mProcessing) + return; + + QString info = i18nc("notification", "Kup procedure is running"); + QString descriptionString; + switch (state.state) { + case ResticProgressState::S_SCANNING: + descriptionString = i18n("Scanning directories..."); + break; + case ResticProgressState::S_TRANSFERING: + descriptionString = i18n("Transfering data..."); + break; + case ResticProgressState::S_BUILDING_NEW_INDEX: + descriptionString = i18n("Building new index..."); + break; + case ResticProgressState::S_COUNTING: + descriptionString = i18n("Counting files..."); + break; + case ResticProgressState::S_FIND_DATA_IN_USE: + descriptionString = i18n("Finding data in use..."); + break; + case ResticProgressState::S_REMOVING: + descriptionString = i18n("Removing old files..."); + break; + } + + if (state.totalItems) { + setTotalAmount(KJob::Files, static_cast(state.totalItems)); + setProcessedAmount(KJob::Files, static_cast(state.processedItems)); + } + + if (state.totalBytes) { + setTotalAmount(KJob::Bytes, static_cast(state.totalBytes)); + setProcessedAmount(KJob::Bytes, static_cast(state.processedBytes)); + } + + setPercent(static_cast(qMin(100, qMax(0, state.mPerc)))); + emitSpeed(static_cast(state.speedBps)); + emit description(this, descriptionString, + QPair(i18n("Executing plan"), mBackupPlan.mDescription), + QPair(i18n("Action"), descriptionString)); + emit infoMessage(this, info, info); +} + +void ResticJob::onBackupCompleted() +{ + mBackupProcess.reset(nullptr); + + mLogStream << endl << QSL("Kup successfully completed the bup backup job at ") + << QLocale().toString(QDateTime::currentDateTime()) << endl; + + QList switches = buildSwitchList(); + if (switches.isEmpty()) { + onForgetCompleted(); + return; + } + + // TODO: I should probably consider a case where the backup succeeds and the + // forget fails. + mForgetProcess.reset(mResticHelper.repoForget(mDestinationPath, switches)); + connect(mForgetProcess.data(), &ResticForgetProcess::forgetFailed, + this, &ResticJob::onForgetFailed); + connect(mForgetProcess.data(), &ResticForgetProcess::forgetSucceeded, + this, &ResticJob::onForgetCompleted); + connect(mForgetProcess.data(), &ResticForgetProcess::forgetStep, + this, &ResticJob::onBackupStep); +} + +void ResticJob::onBackupFailed() +{ + // TODO: Provide more info. + mBackupProcess.reset(nullptr); + mLogStream << endl << QSL("Kup did not successfully complete the bup backup job: " + "failed to save everything.") << endl; + jobFinishedError(ErrorWithLog, xi18nc("@info notification", "Failed to save backup. " + "See log file for more details.")); + mProcessing = false; +} + +void ResticJob::onForgetCompleted() +{ + mForgetProcess.reset(nullptr); + mLogStream << endl << QSL("Kup successfully dropped backups accoding to the policy at ") + << QLocale().toString(QDateTime::currentDateTime()) << endl; + + jobFinishedSuccess(); + mProcessing = false; +} + +void ResticJob::onForgetFailed() +{ + // TODO: provide more info. + mForgetProcess.reset(nullptr); + mLogStream << endl << QSL("Kup could not drop older backups according to the selected policy"); + + jobFinishedError(ErrorWithLog, xi18nc("@info notification", + "Failed to drop older backups")); + mProcessing = false; +} + +bool ResticJob::isCommandAvailable() +{ + QStringList args = QStringList() + << QSL("-c") + << QSL("which restic &> /dev/null"); + return QProcess::execute(QSL("bash"), args) == 0; +} + +bool ResticJob::sendSignal(int signal) +{ + if (mBackupProcess && mBackupProcess->state() != KPtyProcess::NotRunning) { + if (::kill(mBackupProcess->pid(), signal)) { + qCWarning(KUPDAEMON) << "Failed to " + << (signal == SIGSTOP ? "suspend" : "resume") + << " restic backup: " << strerror(errno); + return false; + } + } + + if (mForgetProcess && mForgetProcess->state() != KPtyProcess::NotRunning) { + if (::kill(mForgetProcess->pid(), signal)) { + qCWarning(KUPDAEMON) << "Failed to " + << (signal == SIGSTOP ? "suspend" : "resume") + << " restic forget: " << strerror(errno); + return false; + } + } + + return true; +} + +QList ResticJob::buildSwitchList() +{ + QList switches; + if (mBackupPlan.mKeepLastN && mBackupPlan.mKeepLastNValue > 0) + switches.append(ResticForgetKeepLastN(mBackupPlan.mKeepLastNValue)); + if (mBackupPlan.mKeepHourly && mBackupPlan.mKeepHourlyValue > 0) + switches.append(ResticForgetKeepHourly(mBackupPlan.mKeepHourlyValue)); + if (mBackupPlan.mKeepDaily && mBackupPlan.mKeepDailyValue > 0) + switches.append(ResticForgetKeepDaily(mBackupPlan.mKeepDailyValue)); + if (mBackupPlan.mKeepMonthly && mBackupPlan.mKeepDailyValue > 0) + switches.append(ResticForgetKeepMonthly(mBackupPlan.mKeepMonthlyValue)); + if (mBackupPlan.mKeepYearly && mBackupPlan.mKeepYearlyValue > 0) + switches.append(ResticForgetKeepYearly(mBackupPlan.mKeepYearlyValue)); + if (mBackupPlan.mKeepWithinDuration + && mBackupPlan.mKeepWithinDurationDays > 0 + && mBackupPlan.mKeepWithinDurationMonths > 0 + && mBackupPlan.mKeepWithinDurationYears > 0) + switches.append(ResticForgetKeepWithinDuration( + mBackupPlan.mKeepWithinDurationYears, + mBackupPlan.mKeepWithinDurationMonths, + mBackupPlan.mKeepWithinDurationDays)); + + return switches; +} diff --git a/daemon/resticjob.h b/daemon/resticjob.h new file mode 100644 index 0000000..08cfe29 --- /dev/null +++ b/daemon/resticjob.h @@ -0,0 +1,66 @@ +/*************************************************************************** + * Copyright Luca Carlon * + * carlon.luca@gmail.com * + * * + * 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., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#ifndef RESTICJOB_H +#define RESTICJOB_H + +#include + +#include "backupjob.h" +#include "restichelper.h" + +class ResticJob : public BackupJob +{ + Q_OBJECT +public: + explicit ResticJob(const BackupPlan &pBackupPlan, + const QString &pDestinationPath, + const QString &pLogFilePath, + KupDaemon *pKupDaemon); + +protected: + virtual bool doKill(); + virtual bool doSuspend(); + virtual bool doResume(); + +protected slots: + void performJob() Q_DECL_OVERRIDE; + +private slots: + void onBackupStep(ResticProgressState state); + void onBackupCompleted(); + void onBackupFailed(); + + void onForgetCompleted(); + void onForgetFailed(); + +private: + bool isCommandAvailable(); + bool sendSignal(int signal); + QList buildSwitchList(); + +private: + bool mProcessing; + ResticHelper mResticHelper; + QSharedPointer mBackupProcess; + QSharedPointer mForgetProcess; +}; + +#endif // RESTICJOB_H diff --git a/filedigger/CMakeLists.txt b/filedigger/CMakeLists.txt index e9c1ffc..86dcef5 100644 --- a/filedigger/CMakeLists.txt +++ b/filedigger/CMakeLists.txt @@ -7,11 +7,16 @@ set(filedigger_SRCS filedigger.cpp main.cpp mergedvfs.cpp +mergedvfsbup.cpp +mergedvfsrestic.cpp mergedvfsmodel.cpp restoredialog.cpp restorejob.cpp versionlistdelegate.cpp versionlistmodel.cpp +fsminer.cpp +fsminerbup.cpp +fsminerrestic.cpp ../kioslave/vfshelpers.cpp ../kcm/dirselector.cpp ../settings/kuputils.cpp @@ -29,6 +34,7 @@ add_definitions(-fexceptions) ki18n_wrap_ui(filedigger_SRCS restoredialog.ui) add_executable(kup-filedigger ${filedigger_SRCS}) target_link_libraries(kup-filedigger +resticcore Qt5::Core Qt5::Gui KF5::KIOCore diff --git a/filedigger/filedigger.cpp b/filedigger/filedigger.cpp index 8290ce8..1268fd2 100644 --- a/filedigger/filedigger.cpp +++ b/filedigger/filedigger.cpp @@ -20,9 +20,14 @@ #include "filedigger.h" #include "mergedvfsmodel.h" +#include "mergedvfsrestic.h" #include "restoredialog.h" #include "versionlistmodel.h" #include "versionlistdelegate.h" +#include "fsminer.h" +#include "fsminerrestic.h" +#include "fsminerbup.h" +#include "kupfiledigger_debug.h" #include #include @@ -36,13 +41,72 @@ #include #include +#include #include #include #include #include +#include +#include +#include +#include +#include -FileDigger::FileDigger(const QString &pRepoPath, const QString &pBranchName, QWidget *pParent) - : KMainWindow(pParent), mRepoPath(pRepoPath), mBranchName(pBranchName), mDirOperator(nullptr) +SnapshotListModel::SnapshotListModel(QObject *parent) : QAbstractListModel(parent) +{ + // Do nothing. +} + +void SnapshotListModel::setSnapshots(const QList &snapshots) +{ + mSnapshots = snapshots; + emit dataChanged(index(0, 0), index(snapshots.size() - 1, 0)); +} + +QVariant SnapshotListModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + if (index.row() >= mSnapshots.size()) + return QVariant(); + if (role != Qt::DisplayRole) + return QVariant(); + + return mSnapshots[index.row()].mDateTime.toString(); +} + +int SnapshotListModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return mSnapshots.size(); +} + +BupMountLock::BupMountLock(const QString &repoPath, QObject *parent) : + MountLock(ResticMountManager::generateMountPrivateDir(), parent) +{ + connect(&mProcess, &QProcess::started, + this, &MountLock::servingData); + + QStringList args = QStringList() << QSL("-d") + << repoPath << QSL("fuse") << mMountPath; + mProcess.start(QSL("bup"), args); +} + +bool BupMountLock::isLocked() +{ + return mProcess.state() != QProcess::NotRunning; +} + +bool BupMountLock::unlock() +{ + bool ret = umountMountPoint(); + removeMountPoint(); + + return ret; +} + +FileDigger::FileDigger(BackupType type, const QString &pRepoPath, const QString &pBranchName, QWidget *pParent) + : KMainWindow(pParent), mRepoPath(pRepoPath), mBranchName(pBranchName), mDirOperator(nullptr), mBackupType(type) { setWindowIcon(QIcon::fromTheme(QStringLiteral("kup"))); KToolBar *lAppToolBar = toolBar(); @@ -59,25 +123,43 @@ void FileDigger::updateVersionModel(const QModelIndex &pCurrent, const QModelInd } void FileDigger::open(const QModelIndex &pIndex) { - KRun::runUrl(pIndex.data(VersionBupUrlRole).value(), + KRun::runUrl(pIndex.data(VersionUrlRole).value(), pIndex.data(VersionMimeTypeRole).toString(), this); } void FileDigger::restore(const QModelIndex &pIndex) { - RestoreDialog *lDialog = new RestoreDialog(pIndex.data(VersionSourceInfoRole).value(), this); - lDialog->setAttribute(Qt::WA_DeleteOnClose); - lDialog->show(); + if (mBackupType == BackupType::B_T_BUP) { + RestoreDialog *lDialog = new RestoreDialog(mBackupType, pIndex.data(VersionSourceInfoRole).value(), this); + lDialog->setAttribute(Qt::WA_DeleteOnClose); + lDialog->show(); + } + else { + QMessageBox::warning(this, i18n("Not available"), + i18n("Sorry, the restore procedure is not " + "implemented yet for restic. Use the copy button instead.")); + } +} + +void FileDigger::copy(const QModelIndex &pIndex) +{ + QUrl url = pIndex.data(VersionUrlRole).value(); + if (url.isEmpty()) { + QMessageBox::warning(this, i18n("Cannot find source file"), + i18n("An unknown problem occurred while extracting data.")); + return; + } + + QMimeData* mime = new QMimeData; + mime->setData(QStringLiteral("text/uri-list"), url.toString().toUtf8()); + QApplication::clipboard()->setMimeData(mime); } void FileDigger::repoPathAvailable() { if(mRepoPath.isEmpty()) { createSelectionView(); } else { - MergedRepository *lRepository = createRepo(); - if(lRepository != nullptr) { - createRepoView(lRepository); - } + createRepoView(); } } @@ -93,64 +175,185 @@ void FileDigger::checkFileWidgetPath() { } void FileDigger::enterUrl(QUrl pUrl) { - mDirOperator->setUrl(pUrl, true); + mDirOperator->setUrl(pUrl, true); +} + +void FileDigger::setInfo(QString info) +{ + mInfoLabel->setText(info); +} + +void FileDigger::onBackupDescriptorReceived(BackupDescriptor descriptor) +{ + qCDebug(KUPFILEDIGGER) << "Received descriptor"; + setInfo(i18n("Your backup was opened for reading. " + "Keep this window open while you need it.\n" + "Closing this window will also close the backup.")); + //setBackupOpen(true); + + mDescriptor = descriptor; + mSnapshotModel.setSnapshots(descriptor.mSnapshots); + + mMergedVfsModel = new MergedVfsModel(createRepo(), this); + mMergedVfsView->setModel(mMergedVfsModel); + connect(mMergedVfsView->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), + this, SLOT(updateVersionModel(QModelIndex,QModelIndex))); + + //expand all levels from the top until the node has more than one child + QModelIndex lIndex; + forever { + mMergedVfsView->expand(lIndex); + if(mMergedVfsModel->rowCount(lIndex) == 1) { + lIndex = mMergedVfsModel->index(0, 0, lIndex); + } else { + break; + } + } + mMergedVfsView->selectionModel()->setCurrentIndex(lIndex.child(0,0), QItemSelectionModel::Select); + + VersionListDelegate *lVersionDelegate = new VersionListDelegate(mBackupType, mVersionView, this); + mVersionView->setItemDelegate(lVersionDelegate); + connect(lVersionDelegate, SIGNAL(openRequested(QModelIndex)), SLOT(open(QModelIndex))); + connect(lVersionDelegate, SIGNAL(restoreRequested(QModelIndex)), SLOT(restore(QModelIndex))); + connect(lVersionDelegate, SIGNAL(copyRequested(QModelIndex)), SLOT(copy(QModelIndex))); + mMergedVfsView->setFocus(); + + if (descriptor.mSnapshots.isEmpty()) { + m_tab->setTabEnabled(0, mBackupType == B_T_BUP); + m_tab->setTabEnabled(1, false); + } + else { + m_tab->setTabEnabled(0, true); + m_tab->setTabEnabled(1, true); + } +} + +void FileDigger::requestMount() +{ + if (mBackupType == B_T_RESTIC) { + mMountLock.reset(ResticMountManager::instance().mount(mRepoPath)); + if (!mMountLock) { + setInfo(i18n("Failed to open backup")); + return; + } + + connect(mMountLock.data(), &ResticMountLock::servingData, [this]() { + setInfo(i18n("Reading backup data...")); + mFsMiner.reset(new FsMinerRestic(mMountLock->mountPath())); + connect(mFsMiner.data(), SIGNAL(backupProcessed(BackupDescriptor)), + this, SLOT(onBackupDescriptorReceived(BackupDescriptor))); + mFsMiner->process(); + }); + } + else { + mMountLock.reset(new BupMountLock(mRepoPath)); + + connect(mMountLock.data(), &BupMountLock::servingData, [this]() { + setInfo(i18n("Reading backup data...")); + mFsMiner.reset(new FsMinerBup(mMountLock->mountPath())); + connect(mFsMiner.data(), SIGNAL(backupProcessed(BackupDescriptor)), + this, SLOT(onBackupDescriptorReceived(BackupDescriptor))); + mFsMiner->process(); + }); + } +} + +void FileDigger::closeEvent(QCloseEvent *event) +{ + qWarning() << "closing"; + + if (!mMountLock) + return; + + qWarning() << "Unlocking"; + if (!mMountLock->unlock()) { + event->ignore(); + + QMessageBox::warning(this, + i18n("Failed to close backup"), + i18n("It was not possible to close the backup. " + "Before trying to close the backup, please ensure " + "there is no application trying to use its content. " + "If you are sure no application is trying to use the " + "content of the backup, please try again in a few minutes.")); + + return; + } + + qWarning() << "reset"; + mMountLock.reset(nullptr); } MergedRepository *FileDigger::createRepo() { - MergedRepository *lRepository = new MergedRepository(nullptr, mRepoPath, mBranchName); - if(!lRepository->open()) { + MergedRepository* lRepository; + if (mBackupType == BackupType::B_T_BUP) + lRepository = new MergedRepositoryBup(nullptr, mRepoPath, mBranchName); + else + lRepository = new MergedRepositoryResitc(nullptr, mMountLock->mountPath()); + + if(!lRepository->open()) { KMessageBox::sorry(nullptr, xi18nc("@info messagebox, %1 is a folder path", "The backup archive %1 could not be opened." "Check if the backups really are located there.", mRepoPath)); return nullptr; } + if(!lRepository->readBranch()) { if(!lRepository->permissionsOk()) { KMessageBox::sorry(nullptr, xi18nc("@info messagebox", "You do not have permission needed to read this backup archive.")); } else { - lRepository->askForIntegrityCheck(); + lRepository->rootNode()->askForIntegrityCheck(); } return nullptr; } + return lRepository; } -void FileDigger::createRepoView(MergedRepository *pRepository) { +void FileDigger::createRepoView() { + mInfoLabel = new QLabel; + mInfoLabel->setAlignment(Qt::AlignCenter); + + mSnapshotView = new QListView; + mSnapshotView->setModel(&mSnapshotModel); + connect(mSnapshotView, &QListView::doubleClicked, [this](const QModelIndex &index) { + if (mDescriptor.mSnapshots.size() <= index.row()) + return; + QUrl url = QUrl::fromLocalFile(mDescriptor.mSnapshots[index.row()].mAbsPath); + QDesktopServices::openUrl(url); + }); + QSplitter *lSplitter = new QSplitter(); - mMergedVfsModel = new MergedVfsModel(pRepository, this); + //mMergedVfsModel = new MergedVfsModel(pRepository, this); mMergedVfsView = new QTreeView(); mMergedVfsView->setHeaderHidden(true); mMergedVfsView->setSelectionMode(QAbstractItemView::SingleSelection); - mMergedVfsView->setModel(mMergedVfsModel); lSplitter->addWidget(mMergedVfsView); - connect(mMergedVfsView->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), - this, SLOT(updateVersionModel(QModelIndex,QModelIndex))); mVersionView = new QListView(); mVersionView->setSelectionMode(QAbstractItemView::SingleSelection); mVersionModel = new VersionListModel(this); - mVersionView->setModel(mVersionModel); - VersionListDelegate *lVersionDelegate = new VersionListDelegate(mVersionView,this); - mVersionView->setItemDelegate(lVersionDelegate); - lSplitter->addWidget(mVersionView); - connect(lVersionDelegate, SIGNAL(openRequested(QModelIndex)), SLOT(open(QModelIndex))); - connect(lVersionDelegate, SIGNAL(restoreRequested(QModelIndex)), SLOT(restore(QModelIndex))); - mMergedVfsView->setFocus(); - - //expand all levels from the top until the node has more than one child - QModelIndex lIndex; - forever { - mMergedVfsView->expand(lIndex); - if(mMergedVfsModel->rowCount(lIndex) == 1) { - lIndex = mMergedVfsModel->index(0, 0, lIndex); - } else { - break; - } - } - mMergedVfsView->selectionModel()->setCurrentIndex(lIndex.child(0,0), QItemSelectionModel::Select); - setCentralWidget(lSplitter); + mVersionView->setModel(mVersionModel); + lSplitter->addWidget(mVersionView); + + m_tab = new QTabWidget; + m_tab->addTab(lSplitter, i18n("File digger")); + m_tab->addTab(mSnapshotView, i18n("Snapshots")); + m_tab->setTabEnabled(0, mBackupType == B_T_BUP); + m_tab->setTabEnabled(1, false); + + QVBoxLayout* mainLayout = new QVBoxLayout; + mainLayout->addWidget(mInfoLabel); + mainLayout->addWidget(m_tab); + + QWidget* mainWidget = new QWidget; + mainWidget->setLayout(mainLayout); + + setCentralWidget(mainWidget); + + requestMount(); } void FileDigger::createSelectionView() { diff --git a/filedigger/filedigger.h b/filedigger/filedigger.h index f18d489..799c106 100644 --- a/filedigger/filedigger.h +++ b/filedigger/filedigger.h @@ -24,6 +24,14 @@ #include #include +#include +#include + +#include +#include "fsminer.h" + +#include "vfshelpers.h" + class KDirOperator; class MergedVfsModel; class MergedRepository; @@ -31,33 +39,83 @@ class VersionListModel; class QListView; class QModelIndex; class QTreeView; +class QLabel; +class FsMiner; + +class BupMountLock : public MountLock +{ + Q_OBJECT +public: + BupMountLock(const QString& repoPath, QObject* parent = nullptr); + bool isLocked() override; + +public slots: + bool unlock() override; + +private: + QProcess mProcess; +}; + +class SnapshotListModel : public QAbstractListModel +{ + Q_OBJECT +public: + SnapshotListModel(QObject *parent = nullptr); + void setSnapshots(const QList &snapshots); + QVariant data(const QModelIndex &index, int role) const override; + int rowCount(const QModelIndex &parent) const override; + +private: + QList mSnapshots; +}; class FileDigger : public KMainWindow { Q_OBJECT public: - explicit FileDigger(const QString &pRepoPath, const QString &pBranchName, QWidget *pParent = nullptr); + explicit FileDigger(BackupType type, + const QString &pRepoPath, + const QString &pBranchName, + QWidget *pParent = nullptr); protected slots: void updateVersionModel(const QModelIndex &pCurrent, const QModelIndex &pPrevious); void open(const QModelIndex &pIndex); void restore(const QModelIndex &pIndex); + void copy(const QModelIndex &pIndex); void repoPathAvailable(); void checkFileWidgetPath(); void enterUrl(QUrl pUrl); + void setInfo(QString info); + void onBackupDescriptorReceived(BackupDescriptor desc); + +signals: + void mountSucceeded(); + +protected: + void requestMount(); + virtual void closeEvent(QCloseEvent* event) override; protected: MergedRepository *createRepo(); - void createRepoView(MergedRepository *pRepository); + void createRepoView(); void createSelectionView(); MergedVfsModel *mMergedVfsModel; QTreeView *mMergedVfsView; VersionListModel *mVersionModel; QListView *mVersionView; + QListView *mSnapshotView; QString mRepoPath; QString mBranchName; + QLabel* mInfoLabel; KDirOperator *mDirOperator; + BackupType mBackupType; + QSharedPointer mMountLock; + QSharedPointer mFsMiner; + BackupDescriptor mDescriptor; + SnapshotListModel mSnapshotModel; + QTabWidget* m_tab; }; #endif // FILEDIGGER_H diff --git a/filedigger/fsminer.cpp b/filedigger/fsminer.cpp new file mode 100644 index 0000000..2691a6b --- /dev/null +++ b/filedigger/fsminer.cpp @@ -0,0 +1,33 @@ +/*************************************************************************** + * Copyright Luca Carlon * + * carlon.luca@gmail.com * + * * + * 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., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#include "fsminer.h" + +FsMiner::FsMiner(const QString &mountPath, QObject *parent) : + QObject(parent) + , mMountPath(mountPath) +{ + +} + +void FsMiner::process() +{ + doProcess(); +} diff --git a/filedigger/fsminer.h b/filedigger/fsminer.h new file mode 100644 index 0000000..2ccabcd --- /dev/null +++ b/filedigger/fsminer.h @@ -0,0 +1,59 @@ +/*************************************************************************** + * Copyright Luca Carlon * + * carlon.luca@gmail.com * + * * + * 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., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#ifndef FSMINER_H +#define FSMINER_H + +#include +#include + +struct BackupSnapshot +{ + QDateTime mDateTime; + QString mAbsPath; +}; + +struct BackupDescriptor +{ + QList mSnapshots; +}; + +class FsMiner : public QObject +{ + Q_OBJECT +public: + explicit FsMiner(const QString &mountPath, QObject *parent = nullptr); + +public slots: + void process(); + + virtual QString pathToLatest() = 0; + +signals: + void backupProcessed(BackupDescriptor descriptor); + +protected: + virtual void doProcess() = 0; + +protected: + QString mMountPath; +}; + +#endif // FSMINER_H diff --git a/filedigger/fsminerbup.cpp b/filedigger/fsminerbup.cpp new file mode 100644 index 0000000..fecf4de --- /dev/null +++ b/filedigger/fsminerbup.cpp @@ -0,0 +1,51 @@ +#include + +#include "fsminerbup.h" +#include "kupfiledigger_debug.h" + +FsMinerBup::FsMinerBup(const QString &mountPath, QObject *parent) : + FsMiner(mountPath, parent) +{ + +} + +QString FsMinerBup::pathToLatest() +{ + return QString("%1%2%3%2latest") + .arg(mMountPath).arg(QDir::separator()).arg("kup"); +} + +void FsMinerBup::doProcess() +{ +#define EMIT_AND_RETURN(snapshots) { \ + emit backupProcessed(BackupDescriptor { snapshots }); \ + return; \ + } + +#define EMIT_AND_RETURN_EMPTY EMIT_AND_RETURN(QList()) + + QString snapshotsPath = QString("%1%2kup") + .arg(mMountPath).arg(QDir::separator()); + QDir snapshotsDir(snapshotsPath); + if (!snapshotsDir.exists()) { + qCWarning(KUPFILEDIGGER) << "Cannot find snapshots dir"; + EMIT_AND_RETURN_EMPTY; + } + + QFileInfoList snapshotListDir = snapshotsDir + .entryInfoList(QStringList(), QDir::Dirs | QDir::NoDotAndDotDot); + BackupDescriptor descriptor; + foreach (QFileInfo fileInfo, snapshotListDir) { + QDateTime snapshotDateTime = + QDateTime::fromString(fileInfo.fileName(), QStringLiteral("yyyy-MM-dd-hhmmss")); + if (snapshotDateTime.isNull() || !snapshotDateTime.isValid()) + continue; + BackupSnapshot snaphot { + snapshotDateTime, + fileInfo.absoluteFilePath() + }; + descriptor.mSnapshots.append(snaphot); + } + + emit backupProcessed(descriptor); +} diff --git a/filedigger/fsminerbup.h b/filedigger/fsminerbup.h new file mode 100644 index 0000000..2d14e89 --- /dev/null +++ b/filedigger/fsminerbup.h @@ -0,0 +1,17 @@ +#ifndef FSMINERBUP_H +#define FSMINERBUP_H + +#include "fsminer.h" + +class FsMinerBup : public FsMiner +{ + Q_OBJECT +public: + explicit FsMinerBup(const QString &mountPath, QObject *parent = nullptr); + virtual QString pathToLatest() override; + +protected: + virtual void doProcess() override; +}; + +#endif // FSMINERBUP_H diff --git a/filedigger/fsminerrestic.cpp b/filedigger/fsminerrestic.cpp new file mode 100644 index 0000000..adab16f --- /dev/null +++ b/filedigger/fsminerrestic.cpp @@ -0,0 +1,71 @@ +/*************************************************************************** + * Copyright Luca Carlon * + * carlon.luca@gmail.com * + * * + * 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., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#include + +#include "fsminerrestic.h" +#include "kupfiledigger_debug.h" + +FsMinerRestic::FsMinerRestic(const QString &mountPath, QObject *parent) : + FsMiner(mountPath, parent) +{ + +} + +void FsMinerRestic::doProcess() +{ +#define EMIT_AND_RETURN(snapshots) { \ + emit backupProcessed(BackupDescriptor { snapshots }); \ + return; \ + } + +#define EMIT_AND_RETURN_EMPTY EMIT_AND_RETURN(QList()) + + QString snapshotsPath = QString("%1%2snapshots") + .arg(mMountPath).arg(QDir::separator()); + QDir snapshotsDir(snapshotsPath); + if (!snapshotsDir.exists()) { + qCWarning(KUPFILEDIGGER) << "Cannot find snapshots dir"; + EMIT_AND_RETURN_EMPTY; + } + + QFileInfoList snapshotListDir = snapshotsDir + .entryInfoList(QStringList(), QDir::Dirs | QDir::NoDotAndDotDot); + BackupDescriptor descriptor; + foreach (QFileInfo fileInfo, snapshotListDir) { + QDateTime snapshotDateTime = + QDateTime::fromString(fileInfo.fileName(), Qt::ISODate); + if (snapshotDateTime.isNull() || !snapshotDateTime.isValid()) + continue; + BackupSnapshot snaphot { + snapshotDateTime, + fileInfo.absoluteFilePath() + }; + descriptor.mSnapshots.append(snaphot); + } + + emit backupProcessed(descriptor); +} + +QString FsMinerRestic::pathToLatest() +{ + return QString("%1%2snapshots%2latest") + .arg(mMountPath).arg(QDir::separator()); +} diff --git a/filedigger/fsminerrestic.h b/filedigger/fsminerrestic.h new file mode 100644 index 0000000..820309d --- /dev/null +++ b/filedigger/fsminerrestic.h @@ -0,0 +1,37 @@ +/*************************************************************************** + * Copyright Luca Carlon * + * carlon.luca@gmail.com * + * * + * 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., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#ifndef FSMINERRESTIC_H +#define FSMINERRESTIC_H + +#include "fsminer.h" + +class FsMinerRestic : public FsMiner +{ + Q_OBJECT +public: + explicit FsMinerRestic(const QString &mountPath, QObject *parent = nullptr); + virtual QString pathToLatest() override; + +protected: + virtual void doProcess() override; +}; + +#endif // FSMINERRESTIC_H diff --git a/filedigger/main.cpp b/filedigger/main.cpp index e8541c5..6ed5bae 100644 --- a/filedigger/main.cpp +++ b/filedigger/main.cpp @@ -20,6 +20,7 @@ #include "filedigger.h" #include "mergedvfs.h" +#include "kupfiledigger_debug.h" #if LIBGIT2_VER_MAJOR == 0 && LIBGIT2_VER_MINOR >= 24 #include @@ -57,6 +58,9 @@ int main(int pArgCount, char **pArgArray) { lParser.addOption(QCommandLineOption(QStringList() << QStringLiteral("b") << QStringLiteral("branch"), i18n("Name of the branch to be opened."), QStringLiteral("branch name"), QStringLiteral("kup"))); + lParser.addOption(QCommandLineOption(QStringList() << QStringLiteral("t"), + i18n("Type of backup (bup, restic)."), + QStringLiteral("type of backup"), QStringLiteral("kup"))); lParser.addPositionalArgument(QStringLiteral(""), i18n("Path to the bup repository to be opened.")); lAbout.setupCommandLine(&lParser); @@ -75,7 +79,18 @@ int main(int pArgCount, char **pArgArray) { git_threads_init(); #endif - FileDigger *lFileDigger = new FileDigger(lRepoPath, lParser.value(QStringLiteral("branch"))); + BackupType type; + QString typeString = lParser.value(QStringLiteral("t")); + if (typeString == QStringLiteral("restic")) + type = BackupType::B_T_RESTIC; + else if (typeString == QStringLiteral("bup")) + type = BackupType::B_T_BUP; + else { + qCWarning(KUPFILEDIGGER) << "Please specify a known backup type"; + return 1; + } + + FileDigger *lFileDigger = new FileDigger(type, lRepoPath, lParser.value(QStringLiteral("branch"))); lFileDigger->show(); int lRetVal = lApp.exec(); #if LIBGIT2_VER_MAJOR == 0 && LIBGIT2_VER_MINOR >= 24 diff --git a/filedigger/mergedvfs.cpp b/filedigger/mergedvfs.cpp index 6aeddc0..8cf3a59 100644 --- a/filedigger/mergedvfs.cpp +++ b/filedigger/mergedvfs.cpp @@ -1,6 +1,6 @@ /*************************************************************************** - * Copyright Simon Persson * - * simonpersson1@gmail.com * + * Copyright Luca Carlon * + * carlon.luca@gmail.com * * * * 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 * @@ -25,294 +25,21 @@ #include #include -#include -#include -#include -#include -#include - -typedef QMap NameMap; -typedef QMapIterator NameMapIterator; - -git_repository *MergedNode::mRepository = nullptr; - -bool mergedNodeLessThan(const MergedNode *a, const MergedNode *b) { - if(a->isDirectory() != b->isDirectory()) { - return a->isDirectory(); - } - return a->objectName() < b->objectName(); -} - -bool versionGreaterThan(const VersionData *a, const VersionData *b) { - return a->mModifiedDate > b->mModifiedDate; -} - - -MergedNode::MergedNode(QObject *pParent, const QString &pName, uint pMode) - :QObject(pParent) +MergedNode::MergedNode(QObject *pParent, const QString &pName, uint pMode) : + QObject(pParent) { mSubNodes = nullptr; setObjectName(pName); mMode = pMode; } -void MergedNode::getBupUrl(int pVersionIndex, QUrl *pComplete, QString *pRepoPath, - QString *pBranchName, quint64 *pCommitTime, QString *pPathInRepo) const { - QList lStack; - const MergedNode *lNode = this; - while(lNode != nullptr) { - lStack.append(lNode); - lNode = qobject_cast(lNode->parent()); - } - const MergedRepository *lRepo = qobject_cast(lStack.takeLast()); - if(pComplete) { - pComplete->setUrl("bup://" + lRepo->objectName() + '/' + lRepo->mBranchName + '/' + - vfsTimeToString(mVersionList.at(pVersionIndex)->mCommitTime)); - } - if(pRepoPath) { - *pRepoPath = lRepo->objectName(); - } - if(pBranchName) { - *pBranchName = lRepo->mBranchName; - } - if(pCommitTime) { - *pCommitTime = mVersionList.at(pVersionIndex)->mCommitTime; - } - if(pPathInRepo) { - pPathInRepo->clear(); - } - while(!lStack.isEmpty()) { - QString lPathComponent = lStack.takeLast()->objectName(); - if(pComplete) { - pComplete->setPath(pComplete->path() + '/' + lPathComponent); - } - if(pPathInRepo) { - pPathInRepo->append(QLatin1Char('/')); - pPathInRepo->append(lPathComponent); - } - } -} - MergedNodeList &MergedNode::subNodes() { if(mSubNodes == nullptr) { mSubNodes = new MergedNodeList(); - if(S_ISDIR(mMode)) { + if(isDirectory()) { generateSubNodes(); } } return *mSubNodes; } - -void MergedNode::askForIntegrityCheck() { - int lAnswer = KMessageBox::questionYesNo(nullptr, xi18nc("@info messagebox", - "Could not read this backup archive. Perhaps some files " - "have become corrupted. Do you want to run an integrity " - "check to test this?")); - if(lAnswer == KMessageBox::Yes) { - QDBusInterface lInterface(KUP_DBUS_SERVICE_NAME, KUP_DBUS_OBJECT_PATH); - if(lInterface.isValid()) { - lInterface.call(QStringLiteral("runIntegrityCheck"), - QDir::cleanPath(QString::fromLocal8Bit(git_repository_path(mRepository)))); - } - } -} - -void MergedNode::generateSubNodes() { - NameMap lSubNodeMap; - foreach(VersionData *lCurrentVersion, mVersionList) { - git_tree *lTree; - if(0 != git_tree_lookup(&lTree, mRepository, &lCurrentVersion->mOid)) { - askForIntegrityCheck(); - continue; // try to be fault tolerant by not aborting... - } - git_blob *lMetadataBlob = nullptr; - VintStream *lMetadataStream = nullptr; - const git_tree_entry *lTreeEntry = git_tree_entry_byname(lTree, ".bupm"); - if(lTreeEntry != nullptr && 0 == git_blob_lookup(&lMetadataBlob, mRepository, git_tree_entry_id(lTreeEntry))) { - lMetadataStream = new VintStream(git_blob_rawcontent(lMetadataBlob), git_blob_rawsize(lMetadataBlob), this); - Metadata lMetadata; - readMetadata(*lMetadataStream, lMetadata); // the first entry is metadata for the directory itself, discard it. - } - - uint lEntryCount = git_tree_entrycount(lTree); - for(uint i = 0; i < lEntryCount; ++i) { - uint lMode; - const git_oid *lOid; - QString lName; - bool lChunked; - const git_tree_entry *lTreeEntry = git_tree_entry_byindex(lTree, i); - getEntryAttributes(lTreeEntry, lMode, lChunked, lOid, lName); - if(lName == QStringLiteral(".bupm")) { - continue; - } - - MergedNode *lSubNode = lSubNodeMap.value(lName, nullptr); - if(lSubNode == nullptr) { - lSubNode = new MergedNode(this, lName, lMode); - lSubNodeMap.insert(lName, lSubNode); - mSubNodes->append(lSubNode); - } else if((S_IFMT & lMode) != (S_IFMT & lSubNode->mMode)) { - if(S_ISDIR(lMode)) { - lName.append(xi18nc("added after folder name in some cases", " (folder)")); - } else if(S_ISLNK(lMode)) { - lName.append(xi18nc("added after file name in some cases", " (symlink)")); - } else { - lName.append(xi18nc("added after file name in some cases", " (file)")); - } - lSubNode = lSubNodeMap.value(lName, nullptr); - if(lSubNode == nullptr) { - lSubNode = new MergedNode(this, lName, lMode); - lSubNodeMap.insert(lName, lSubNode); - mSubNodes->append(lSubNode); - } - } - bool lAlreadySeen = false; - foreach(VersionData *lVersion, lSubNode->mVersionList) { - if(lVersion->mOid == *lOid) { - lAlreadySeen = true; - break; - } - } - if(S_ISDIR(lMode)) { - if(!lAlreadySeen) { - lSubNode->mVersionList.append(new VersionData(lOid, lCurrentVersion->mCommitTime, - lCurrentVersion->mModifiedDate, 0)); - } - } else { - quint64 lModifiedDate; - Metadata lMetadata; - if(lMetadataStream != nullptr && 0 == readMetadata(*lMetadataStream, lMetadata)) { - lModifiedDate = lMetadata.mMtime; - } else { - lModifiedDate = lCurrentVersion->mModifiedDate; - } - if(!lAlreadySeen) { - lSubNode->mVersionList.append(new VersionData(lChunked, lOid, - lCurrentVersion->mCommitTime, - lModifiedDate)); - } - } - } - if(lMetadataStream != nullptr) { - delete lMetadataStream; - git_blob_free(lMetadataBlob); - } - git_tree_free(lTree); - } - qSort(mSubNodes->begin(), mSubNodes->end(), mergedNodeLessThan); - foreach(MergedNode *lNode, *mSubNodes) { - qSort(lNode->mVersionList.begin(), lNode->mVersionList.end(), versionGreaterThan); - } -} - -MergedRepository::MergedRepository(QObject *pParent, const QString &pRepositoryPath, const QString &pBranchName) - : MergedNode(pParent, pRepositoryPath, DEFAULT_MODE_DIRECTORY), mBranchName(pBranchName) -{ - if(!objectName().endsWith(QLatin1Char('/'))) { - setObjectName(objectName() + QLatin1Char('/')); - } -} - -MergedRepository::~MergedRepository() { - if(mRepository != nullptr) { - git_repository_free(mRepository); - } -} - -bool MergedRepository::open() { - if(0 != git_repository_open(&mRepository, objectName().toLocal8Bit())) { - qCWarning(KUPFILEDIGGER) << "could not open repository " << objectName(); - mRepository = nullptr; - return false; - } - return true; -} - -bool MergedRepository::readBranch() { - if(mRepository == nullptr) { - return false; - } - git_revwalk *lRevisionWalker; - if(0 != git_revwalk_new(&lRevisionWalker, mRepository)) { - qCWarning(KUPFILEDIGGER) << "could not create a revision walker in repository " << objectName(); - return false; - } - - QString lCompleteBranchName = QStringLiteral("refs/heads/"); - lCompleteBranchName.append(mBranchName); - if(0 != git_revwalk_push_ref(lRevisionWalker, lCompleteBranchName.toLocal8Bit())) { - qCWarning(KUPFILEDIGGER) << "Unable to read branch " << mBranchName << " in repository " << objectName(); - git_revwalk_free(lRevisionWalker); - return false; - } - bool lEmptyList = true; - git_oid lOid; - while(0 == git_revwalk_next(&lOid, lRevisionWalker)) { - git_commit *lCommit; - if(0 != git_commit_lookup(&lCommit, mRepository, &lOid)) { - continue; - } - git_time_t lTime = git_commit_time(lCommit); - mVersionList.append(new VersionData(git_commit_tree_id(lCommit), lTime, lTime, 0)); - lEmptyList = false; - git_commit_free(lCommit); - } - git_revwalk_free(lRevisionWalker); - return !lEmptyList; -} - -bool MergedRepository::permissionsOk() { - if(mRepository == nullptr) { - return false; - } - QDir lRepoDir(objectName()); - if(!lRepoDir.exists()) { - return false; - } - QList lDirectories; - lDirectories << lRepoDir; - while(!lDirectories.isEmpty()) { - QDir lDir = lDirectories.takeFirst(); - foreach(QFileInfo lFileInfo, lDir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot)) { - if(!lFileInfo.isReadable()) { - return false; - } - if(lFileInfo.isDir()) { - lDirectories << QDir(lFileInfo.absoluteFilePath()); - } - } - } - return true; -} - -uint qHash(git_oid pOid) { - return qHash(QByteArray::fromRawData((char *)pOid.id, GIT_OID_RAWSZ)); -} - - -bool operator ==(const git_oid &pOidA, const git_oid &pOidB) { - QByteArray a = QByteArray::fromRawData((char *)pOidA.id, GIT_OID_RAWSZ); - QByteArray b = QByteArray::fromRawData((char *)pOidB.id, GIT_OID_RAWSZ); - return a == b; -} - - -quint64 VersionData::size() { - if(mSizeIsValid) { - return mSize; - } - if(mChunkedFile) { - mSize = calculateChunkFileSize(&mOid, MergedNode::mRepository); - } else { - git_blob *lBlob; - if(0 == git_blob_lookup(&lBlob, MergedNode::mRepository, &mOid)) { - mSize = git_blob_rawsize(lBlob); - git_blob_free(lBlob); - } else { - mSize = 0; - } - } - mSizeIsValid = true; - return mSize; -} diff --git a/filedigger/mergedvfs.h b/filedigger/mergedvfs.h index b84bc67..c928677 100644 --- a/filedigger/mergedvfs.h +++ b/filedigger/mergedvfs.h @@ -1,6 +1,6 @@ /*************************************************************************** - * Copyright Simon Persson * - * simonpersson1@gmail.com * + * Copyright Luca Carlon * + * carlon.luca@gmail.com * * * * 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 * @@ -21,33 +21,36 @@ #ifndef MERGEDVFS_H #define MERGEDVFS_H -#include -uint qHash(git_oid pOid); -bool operator ==(const git_oid &pOidA, const git_oid &pOidB); #include #include - #include +#include #include +class MergedNode; +typedef QMap NameMap; +typedef QMapIterator NameMapIterator; + struct VersionData { - VersionData(bool pChunkedFile, const git_oid *pOid, quint64 pCommitTime, quint64 pModifiedDate) - :mChunkedFile(pChunkedFile), mOid(*pOid), mCommitTime(pCommitTime), mModifiedDate(pModifiedDate) + VersionData(quint64 pCommitTime, quint64 pModifiedDate) + :mCommitTime(pCommitTime), mModifiedDate(pModifiedDate) { mSizeIsValid = false; } - VersionData(const git_oid *pOid, quint64 pCommitTime, quint64 pModifiedDate, quint64 pSize) - :mOid(*pOid), mCommitTime(pCommitTime), mModifiedDate(pModifiedDate), mSize(pSize) + VersionData(quint64 pCommitTime, quint64 pModifiedDate, quint64 pSize) + :mCommitTime(pCommitTime), mModifiedDate(pModifiedDate), mSize(pSize) { mSizeIsValid = true; } - quint64 size(); + virtual ~VersionData() {} + + virtual quint64 size() = 0; + bool mSizeIsValid; - bool mChunkedFile; - git_oid mOid; + quint64 mCommitTime; quint64 mModifiedDate; @@ -55,7 +58,6 @@ struct VersionData { quint64 mSize; }; -class MergedNode; typedef QList MergedNodeList; typedef QListIterator MergedNodeListIterator; typedef QList VersionList; @@ -68,37 +70,37 @@ class MergedNode: public QObject { MergedNode(QObject *pParent, const QString &pName, uint pMode); virtual ~MergedNode() { if(mSubNodes != nullptr) { + qDeleteAll(*mSubNodes); delete mSubNodes; } } bool isDirectory() const { return S_ISDIR(mMode); } - void getBupUrl(int pVersionIndex, QUrl *pComplete, QString *pRepoPath = nullptr, QString *pBranchName = nullptr, - quint64 *pCommitTime = nullptr, QString *pPathInRepo = nullptr) const; virtual MergedNodeList &subNodes(); const VersionList *versionList() const { return &mVersionList; } - uint mode() const { return mMode; } - static void askForIntegrityCheck(); -protected: - virtual void generateSubNodes(); + virtual void getUrl(int pVersionIndex, QUrl *pComplete, QString *pRepoPath = nullptr, QString *pBranchName = nullptr, + quint64 *pCommitTime = nullptr, QString *pPathInRepo = nullptr) const = 0; + virtual void askForIntegrityCheck() = 0; - static git_repository *mRepository; - uint mMode; +public: VersionList mVersionList; MergedNodeList *mSubNodes; + uint mMode; + +protected: + virtual void generateSubNodes() = 0; }; -class MergedRepository: public MergedNode { +class MergedRepository : public QObject +{ Q_OBJECT public: - MergedRepository(QObject *pParent, const QString &pRepositoryPath, const QString &pBranchName); - virtual ~MergedRepository(); - - bool open(); - bool readBranch(); - bool permissionsOk(); - - QString mBranchName; + MergedRepository(QObject* parent = nullptr) : QObject(parent) {} + virtual ~MergedRepository() {} + virtual bool open() = 0; + virtual bool readBranch() = 0; + virtual bool permissionsOk() = 0; + virtual MergedNode *rootNode() const = 0; }; #endif // MERGEDVFS_H diff --git a/filedigger/mergedvfsbup.cpp b/filedigger/mergedvfsbup.cpp new file mode 100644 index 0000000..e6c5961 --- /dev/null +++ b/filedigger/mergedvfsbup.cpp @@ -0,0 +1,312 @@ +/*************************************************************************** + * Copyright Simon Persson * + * simonpersson1@gmail.com * + * * + * 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., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#include +#include + +#include +#include + +#include +#include + +#include "mergedvfsbup.h" +#include "vfshelpers.h" +#include "kupdaemon.h" +#include "kupfiledigger_debug.h" + +git_repository *MergedNodeBup::mRepository = nullptr; + +bool mergedNodeLessThan(const MergedNode *a, const MergedNode *b) { + if(a->isDirectory() != b->isDirectory()) { + return a->isDirectory(); + } + return a->objectName() < b->objectName(); +} + +bool versionGreaterThan(const VersionData *a, const VersionData *b) { + return a->mModifiedDate > b->mModifiedDate; +} + +MergedNodeBup::MergedNodeBup(QObject *pParent, const QString &pName, uint pMode) : + MergedNode(pParent, pName, pMode) +{ +} + +void MergedNodeBup::generateSubNodes() +{ + NameMap lSubNodeMap; + foreach(VersionData *_lCurrentVersion, mVersionList) { + VersionDataBup* lCurrentVersion = static_cast(_lCurrentVersion); + git_tree *lTree; + if(0 != git_tree_lookup(&lTree, mRepository, &lCurrentVersion->mOid)) { + askForIntegrityCheck(); + continue; // try to be fault tolerant by not aborting... + } + git_blob *lMetadataBlob = nullptr; + VintStream *lMetadataStream = nullptr; + const git_tree_entry *lTreeEntry = git_tree_entry_byname(lTree, ".bupm"); + if(lTreeEntry != nullptr && 0 == git_blob_lookup(&lMetadataBlob, mRepository, git_tree_entry_id(lTreeEntry))) { + lMetadataStream = new VintStream(git_blob_rawcontent(lMetadataBlob), git_blob_rawsize(lMetadataBlob), this); + Metadata lMetadata; + readMetadata(*lMetadataStream, lMetadata); // the first entry is metadata for the directory itself, discard it. + } + + uint lEntryCount = git_tree_entrycount(lTree); + for(uint i = 0; i < lEntryCount; ++i) { + uint lMode; + const git_oid *lOid; + QString lName; + bool lChunked; + const git_tree_entry *lTreeEntry = git_tree_entry_byindex(lTree, i); + getEntryAttributes(lTreeEntry, lMode, lChunked, lOid, lName); + if(lName == QStringLiteral(".bupm")) { + continue; + } + + MergedNode *lSubNode = lSubNodeMap.value(lName, nullptr); + if(lSubNode == nullptr) { + lSubNode = new MergedNodeBup(this, lName, lMode); + lSubNodeMap.insert(lName, lSubNode); + mSubNodes->append(lSubNode); + } else if((S_IFMT & lMode) != (S_IFMT & lSubNode->mMode)) { + if(S_ISDIR(lMode)) { + lName.append(xi18nc("added after folder name in some cases", " (folder)")); + } else if(S_ISLNK(lMode)) { + lName.append(xi18nc("added after file name in some cases", " (symlink)")); + } else { + lName.append(xi18nc("added after file name in some cases", " (file)")); + } + lSubNode = lSubNodeMap.value(lName, nullptr); + if(lSubNode == nullptr) { + lSubNode = new MergedNodeBup(this, lName, lMode); + lSubNodeMap.insert(lName, lSubNode); + mSubNodes->append(lSubNode); + } + } + bool lAlreadySeen = false; + foreach(VersionData *_lVersion, lSubNode->mVersionList) { + VersionDataBup *lVersion = static_cast(_lVersion); + if(lVersion->mOid == *lOid) { + lAlreadySeen = true; + break; + } + } + if(S_ISDIR(lMode)) { + if(!lAlreadySeen) { + lSubNode->mVersionList.append(new VersionDataBup(lOid, lCurrentVersion->mCommitTime, + lCurrentVersion->mModifiedDate, 0)); + } + } else { + quint64 lModifiedDate; + Metadata lMetadata; + if(lMetadataStream != nullptr && 0 == readMetadata(*lMetadataStream, lMetadata)) { + lModifiedDate = lMetadata.mMtime; + } else { + lModifiedDate = lCurrentVersion->mModifiedDate; + } + if(!lAlreadySeen) { + lSubNode->mVersionList.append(new VersionDataBup(lChunked, lOid, + lCurrentVersion->mCommitTime, + lModifiedDate)); + } + } + } + if(lMetadataStream != nullptr) { + delete lMetadataStream; + git_blob_free(lMetadataBlob); + } + git_tree_free(lTree); + } + qSort(mSubNodes->begin(), mSubNodes->end(), mergedNodeLessThan); + foreach(MergedNode *lNode, *mSubNodes) { + qSort(lNode->mVersionList.begin(), lNode->mVersionList.end(), versionGreaterThan); + } +} + +void MergedNodeBup::getUrl(int pVersionIndex, QUrl *pComplete, QString *pRepoPath, QString *pBranchName, quint64 *pCommitTime, QString *pPathInRepo) const +{ + QList lStack; + const MergedNode *lNode = this; + while(lNode != nullptr) { + lStack.append(lNode); + lNode = qobject_cast(lNode->parent()); + } + const MergedRepositoryBup *lRepo = qobject_cast(lStack.takeLast()->parent()); + if(pComplete) { + pComplete->setUrl("bup://" + lRepo->rootNode()->objectName() + '/' + lRepo->mBranchName + '/' + + vfsTimeToString(mVersionList.at(pVersionIndex)->mCommitTime)); + } + if(pRepoPath) { + *pRepoPath = lRepo->objectName(); + } + if(pBranchName) { + *pBranchName = lRepo->mBranchName; + } + if(pCommitTime) { + *pCommitTime = mVersionList.at(pVersionIndex)->mCommitTime; + } + if(pPathInRepo) { + pPathInRepo->clear(); + } + while(!lStack.isEmpty()) { + QString lPathComponent = lStack.takeLast()->objectName(); + if(pComplete) { + pComplete->setPath(pComplete->path() + '/' + lPathComponent); + } + if(pPathInRepo) { + pPathInRepo->append(QLatin1Char('/')); + pPathInRepo->append(lPathComponent); + } + } +} + +void MergedNodeBup::askForIntegrityCheck() +{ + int lAnswer = KMessageBox::questionYesNo(nullptr, xi18nc("@info messagebox", + "Could not read this backup archive. Perhaps some files " + "have become corrupted. Do you want to run an integrity " + "check to test this?")); + if(lAnswer == KMessageBox::Yes) { + QDBusInterface lInterface(KUP_DBUS_SERVICE_NAME, KUP_DBUS_OBJECT_PATH); + if(lInterface.isValid()) { + lInterface.call(QStringLiteral("runIntegrityCheck"), + QDir::cleanPath(QString::fromLocal8Bit(git_repository_path(MergedNodeBup::mRepository)))); + } + } +} + +MergedRepositoryBup::MergedRepositoryBup(QObject *pParent, const QString &pRepositoryPath, const QString &pBranchName) + : MergedRepository(pParent), + mBranchName(pBranchName) + , mRoot(new MergedNodeBup(this, pRepositoryPath, DEFAULT_MODE_DIRECTORY)) +{ + if(!objectName().endsWith(QLatin1Char('/'))) { + setObjectName(objectName() + QLatin1Char('/')); + } +} + +MergedRepositoryBup::~MergedRepositoryBup() { + if(MergedNodeBup::mRepository != nullptr) { + git_repository_free(MergedNodeBup::mRepository); + } +} + +bool MergedRepositoryBup::open() { + if(0 != git_repository_open(&MergedNodeBup::mRepository, rootNode()->objectName().toLocal8Bit())) { + qCWarning(KUPFILEDIGGER) << "could not open repository " << objectName(); + MergedNodeBup::mRepository = nullptr; + return false; + } + return true; +} + +bool MergedRepositoryBup::readBranch() { + if(MergedNodeBup::mRepository == nullptr) { + return false; + } + git_revwalk *lRevisionWalker; + if(0 != git_revwalk_new(&lRevisionWalker, MergedNodeBup::mRepository)) { + qCWarning(KUPFILEDIGGER) << "could not create a revision walker in repository " << objectName(); + return false; + } + + QString lCompleteBranchName = QStringLiteral("refs/heads/"); + lCompleteBranchName.append(mBranchName); + if(0 != git_revwalk_push_ref(lRevisionWalker, lCompleteBranchName.toLocal8Bit())) { + qCWarning(KUPFILEDIGGER) << "Unable to read branch " << mBranchName << " in repository " << objectName(); + git_revwalk_free(lRevisionWalker); + return false; + } + bool lEmptyList = true; + git_oid lOid; + while(0 == git_revwalk_next(&lOid, lRevisionWalker)) { + git_commit *lCommit; + if(0 != git_commit_lookup(&lCommit, MergedNodeBup::mRepository, &lOid)) { + continue; + } + git_time_t lTime = git_commit_time(lCommit); + rootNode()->mVersionList.append(new VersionDataBup(git_commit_tree_id(lCommit), lTime, lTime, 0)); + lEmptyList = false; + git_commit_free(lCommit); + } + git_revwalk_free(lRevisionWalker); + return !lEmptyList; +} + +bool MergedRepositoryBup::permissionsOk() { + if(MergedNodeBup::mRepository == nullptr) { + return false; + } + QDir lRepoDir(objectName()); + if(!lRepoDir.exists()) { + return false; + } + QList lDirectories; + lDirectories << lRepoDir; + while(!lDirectories.isEmpty()) { + QDir lDir = lDirectories.takeFirst(); + foreach(QFileInfo lFileInfo, lDir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot)) { + if(!lFileInfo.isReadable()) { + return false; + } + if(lFileInfo.isDir()) { + lDirectories << QDir(lFileInfo.absoluteFilePath()); + } + } + } + return true; +} + +MergedNode *MergedRepositoryBup::rootNode() const +{ + return mRoot; +} + +uint qHash(git_oid pOid) { + return qHash(QByteArray::fromRawData((char *)pOid.id, GIT_OID_RAWSZ)); +} + + +bool operator ==(const git_oid &pOidA, const git_oid &pOidB) { + QByteArray a = QByteArray::fromRawData((char *)pOidA.id, GIT_OID_RAWSZ); + QByteArray b = QByteArray::fromRawData((char *)pOidB.id, GIT_OID_RAWSZ); + return a == b; +} + +quint64 VersionDataBup::size() +{ + if(mSizeIsValid) { + return mSize; + } + if(mChunkedFile) { + mSize = calculateChunkFileSize(&mOid, MergedNodeBup::mRepository); + } else { + git_blob *lBlob; + if(0 == git_blob_lookup(&lBlob, MergedNodeBup::mRepository, &mOid)) { + mSize = git_blob_rawsize(lBlob); + git_blob_free(lBlob); + } else { + mSize = 0; + } + } + mSizeIsValid = true; + return mSize; +} diff --git a/filedigger/mergedvfsbup.h b/filedigger/mergedvfsbup.h new file mode 100644 index 0000000..9699f5f --- /dev/null +++ b/filedigger/mergedvfsbup.h @@ -0,0 +1,78 @@ +/*************************************************************************** + * Copyright Simon Persson * + * simonpersson1@gmail.com * + * * + * 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., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#ifndef MERGEDVFSBUP_H +#define MERGEDVFSBUP_H + +#include +uint qHash(git_oid pOid); +bool operator ==(const git_oid &pOidA, const git_oid &pOidB); +#include "mergedvfs.h" + +struct VersionDataBup : public VersionData +{ + VersionDataBup(bool pChunkedFile, const git_oid *pOid, quint64 pCommitTime, quint64 pModifiedDate) : + VersionData(pCommitTime, pModifiedDate), mOid(*pOid), mChunkedFile(pChunkedFile) {} + VersionDataBup(const git_oid *pOid, quint64 pCommitTime, quint64 pModifiedDate, quint64 pSize) : + VersionData(pCommitTime, pModifiedDate, pSize), mOid(*pOid) {} + + virtual quint64 size(); + + git_oid mOid; + bool mChunkedFile; +}; + +class MergedNodeBup : public MergedNode +{ + Q_OBJECT + friend class VersionDataBup; +public: + MergedNodeBup(QObject *pParent, const QString &pName, uint pMode); + virtual void getUrl(int pVersionIndex, QUrl *pComplete, QString *pRepoPath = nullptr, QString *pBranchName = nullptr, + quint64 *pCommitTime = nullptr, QString *pPathInRepo = nullptr) const; + virtual void askForIntegrityCheck(); + +protected: + virtual void generateSubNodes(); + +public: + static git_repository *mRepository; + +private: + uint mMode; +}; + +class MergedRepositoryBup: public MergedRepository +{ + Q_OBJECT +public: + MergedRepositoryBup(QObject *pParent, const QString &pRepositoryPath, const QString &pBranchName); + virtual ~MergedRepositoryBup(); + + virtual bool open(); + virtual bool readBranch(); + virtual bool permissionsOk(); + virtual MergedNode *rootNode() const; + + QString mBranchName; + MergedNodeBup *mRoot; +}; + +#endif // MERGEDVFSBUP_H diff --git a/filedigger/mergedvfsmodel.cpp b/filedigger/mergedvfsmodel.cpp index f423d5d..531877a 100644 --- a/filedigger/mergedvfsmodel.cpp +++ b/filedigger/mergedvfsmodel.cpp @@ -62,10 +62,10 @@ QModelIndex MergedVfsModel::index(int pRow, int pColumn, const QModelIndex &pPar return QModelIndex(); // invalid } if(!pParent.isValid()) { - if(pRow >= mRoot->subNodes().count()) { + if(pRow >= mRoot->rootNode()->subNodes().count()) { return QModelIndex(); // invalid } - return createIndex(pRow, 0, mRoot->subNodes().at(pRow)); + return createIndex(pRow, 0, mRoot->rootNode()->subNodes().at(pRow)); } MergedNode *lParentNode = static_cast(pParent.internalPointer()); if(pRow >= lParentNode->subNodes().count()) { @@ -80,7 +80,7 @@ QModelIndex MergedVfsModel::parent(const QModelIndex &pChild) const { } MergedNode *lChild = static_cast(pChild.internalPointer()); MergedNode *lParent = qobject_cast(lChild->parent()); - if(lParent == nullptr || lParent == mRoot) { + if(lParent == nullptr || lParent == mRoot->rootNode()) { return QModelIndex(); //invalid } MergedNode *lGrandParent = qobject_cast(lParent->parent()); @@ -92,7 +92,7 @@ QModelIndex MergedVfsModel::parent(const QModelIndex &pChild) const { int MergedVfsModel::rowCount(const QModelIndex &pParent) const { if(!pParent.isValid()) { - return mRoot->subNodes().count(); + return mRoot->rootNode()->subNodes().count(); } MergedNode *lParent = static_cast(pParent.internalPointer()); if(lParent == nullptr) { diff --git a/filedigger/mergedvfsmodel.h b/filedigger/mergedvfsmodel.h index bc70f72..5a4fb70 100644 --- a/filedigger/mergedvfsmodel.h +++ b/filedigger/mergedvfsmodel.h @@ -24,11 +24,13 @@ #include #include "mergedvfs.h" +#include "mergedvfsbup.h" class MergedVfsModel : public QAbstractItemModel { Q_OBJECT public: + // TODO explicit MergedVfsModel(MergedRepository *pRoot, QObject *pParent = 0); ~MergedVfsModel(); int columnCount(const QModelIndex &pParent) const; @@ -41,6 +43,7 @@ class MergedVfsModel : public QAbstractItemModel const MergedNode *node(const QModelIndex &pIndex); protected: + // TODO MergedRepository *mRoot; }; diff --git a/filedigger/mergedvfsrestic.cpp b/filedigger/mergedvfsrestic.cpp new file mode 100644 index 0000000..7c2c532 --- /dev/null +++ b/filedigger/mergedvfsrestic.cpp @@ -0,0 +1,115 @@ +/*************************************************************************** + * Copyright Luca Carlon * + * carlon.luca@gmail.com * + * * + * 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., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#include +#include + +#include "mergedvfsrestic.h" +#include "vfshelpers.h" +#include "kuputils.h" + +MergedNodeRestic::MergedNodeRestic(QObject *parent, const QString &repoPath, const QString &relPath, const QString &name, uint mode) : + MergedNode(parent, name, mode) + , mSnapthosPath(repoPath) + , mRelPath(relPath) + , mRegexPath("([\\/][^\\/]+)+\\/snapshots\\/([^\\/]+)\\/(.*)$") +{ +} + +void MergedNodeRestic::getUrl(int pVersionIndex, QUrl *pComplete, QString *pRepoPath, QString *pBranchName, quint64 *pCommitTime, QString *pPathInRepo) const +{ + if (mVersionList.size() <= pVersionIndex) { + *pComplete = QUrl(); + return; + } + + VersionDataRestic* version = static_cast(mVersionList[pVersionIndex]); + + if (pComplete) + *pComplete = QUrl::fromLocalFile(version->absPath()); + if (pRepoPath) + *pRepoPath = "repo"; // TODO + if (pBranchName) + *pBranchName = QString(); + if (pCommitTime) + *pCommitTime = version->mCommitTime; + if (pPathInRepo) + *pPathInRepo = QString(); +} + +void MergedNodeRestic::generateSubNodes() +{ + QHash nodes; + QDir snapshots(mSnapthosPath); + QFileInfoList infoList = snapshots.entryInfoList(QStringList(), + QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot, + QDir::Name | QDir::Reversed); + foreach (const QFileInfo &info, infoList) { + QString pathToSearch = QString("%1%2%3") + .arg(info.absoluteFilePath()).arg(QDir::separator()).arg(mRelPath); + QFileInfoList itemList = QDir(pathToSearch).entryInfoList(QStringList(), + QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot); + foreach (const QFileInfo &item, itemList) { + QRegularExpressionMatch match = mRegexPath.match(item.absoluteFilePath()); + if (!match.hasMatch()) + continue; + QDateTime dateTime = QDateTime::fromString(match.captured(2), Qt::ISODate); + QString relPath = match.captured(3); + MergedNode* node = nodes.value(relPath); + if (!node) { + uint mode = item.isDir() ? DEFAULT_MODE_DIRECTORY : DEFAULT_MODE_FILE; + node = new MergedNodeRestic(this, mSnapthosPath, relPath, item.fileName(), mode); + nodes.insert(relPath, node); + } + + // Compare. + if (!node->mVersionList.isEmpty()) { + VersionDataRestic *last = static_cast(node->mVersionList.last()); + QString absPath1 = last->absPath(); + QString absPath2 = item.absoluteFilePath(); + if (!fastDiff(absPath1, absPath2)) + continue; + } + + node->mVersionList.append(new VersionDataRestic( + dateTime.toSecsSinceEpoch(), + item.lastModified().toSecsSinceEpoch(), + static_cast(item.size()), + item.absoluteFilePath() + )); + } + } + + if (!mSubNodes) + mSubNodes = new QList(nodes.values()); + else + mSubNodes->append(nodes.values()); +} + +MergedRepositoryResitc::MergedRepositoryResitc(QObject *pParent, const QString &pRepoMountPath) : + MergedRepository(pParent) + , mRoot(new MergedNodeRestic(pParent, QString("%1%2snapshots").arg(pRepoMountPath).arg(QDir::separator()), QStringLiteral("/"), QStringLiteral("/"), DEFAULT_MODE_DIRECTORY)) +{ +} + +MergedRepositoryResitc::~MergedRepositoryResitc() +{ + +} diff --git a/filedigger/mergedvfsrestic.h b/filedigger/mergedvfsrestic.h new file mode 100644 index 0000000..b8579b8 --- /dev/null +++ b/filedigger/mergedvfsrestic.h @@ -0,0 +1,78 @@ +/*************************************************************************** + * Copyright Luca Carlon * + * carlon.luca@gmail.com * + * * + * 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., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#ifndef MERGEDVFSRESTIC_H +#define MERGEDVFSRESTIC_H + +#include +#include + +#include "mergedvfs.h" + +class VersionDataRestic : public VersionData +{ +public: + VersionDataRestic(quint64 pCommitTime, quint64 pModifiedDate, quint64 size, const QString &absPath) : + VersionData(pCommitTime, pModifiedDate), mSize(size), mAbsPath(absPath) {} + quint64 size() { return mSize; } + QString absPath() { return mAbsPath; } + +private: + quint64 mSize; + QString mAbsPath; +}; + +class MergedNodeRestic : public MergedNode +{ +public: + MergedNodeRestic(QObject *parent, + const QString &repoPath, + const QString &relPath, + const QString &name, + uint mode); + virtual void getUrl(int pVersionIndex, QUrl *pComplete, QString *pRepoPath = nullptr, QString *pBranchName = nullptr, + quint64 *pCommitTime = nullptr, QString *pPathInRepo = nullptr) const; + virtual void askForIntegrityCheck() {} + +protected: + void generateSubNodes(); + +private: + QString mSnapthosPath; + QString mRelPath; + QRegularExpression mRegexPath; +}; + +class MergedRepositoryResitc : public MergedRepository +{ + Q_OBJECT +public: + MergedRepositoryResitc(QObject *pParent, const QString &pRepositoryPath); + virtual ~MergedRepositoryResitc(); + + virtual bool open() { return true; } + virtual bool readBranch() { return true; } + virtual bool permissionsOk() { return true; } + virtual MergedNode *rootNode() const { return mRoot; } + + MergedNodeRestic *mRoot; +}; + +#endif // MERGEDVFSRESTIC_H diff --git a/filedigger/restoredialog.cpp b/filedigger/restoredialog.cpp index f9fd3ee..0bf5889 100644 --- a/filedigger/restoredialog.cpp +++ b/filedigger/restoredialog.cpp @@ -42,8 +42,8 @@ #define KUP_TMP_RESTORE_FOLDER QStringLiteral("_kup_temporary_restore_folder_") -RestoreDialog::RestoreDialog(const BupSourceInfo &pPathInfo, QWidget *parent) - : QDialog(parent), mUI(new Ui::RestoreDialog), mSourceInfo(pPathInfo) +RestoreDialog::RestoreDialog(BackupType backupType, const BupSourceInfo &pPathInfo, QWidget *parent) + : QDialog(parent), mUI(new Ui::RestoreDialog), mSourceInfo(pPathInfo), mBackupType(backupType) { mSourceFileName = mSourceInfo.mPathInRepo.section(QDir::separator(), -1); @@ -56,6 +56,10 @@ RestoreDialog::RestoreDialog(const BupSourceInfo &pPathInfo, QWidget *parent) mUI->mRestoreOriginalButton->setMinimumHeight(mUI->mRestoreOriginalButton->sizeHint().height() * 2); mUI->mRestoreCustomButton->setMinimumHeight(mUI->mRestoreCustomButton->sizeHint().height() * 2); + // Cannot do this at the moment for restic. + if (backupType == BackupType::B_T_RESTIC) + mUI->mRestoreOriginalButton->setEnabled(false); + connect(mUI->mRestoreOriginalButton, SIGNAL(clicked()), SLOT(setOriginalDestination())); connect(mUI->mRestoreCustomButton, SIGNAL(clicked()), SLOT(setCustomDestination())); @@ -116,12 +120,17 @@ void RestoreDialog::setCustomDestination() { connect(lNewFolderButton, SIGNAL(clicked()), SLOT(createNewFolder())); mUI->mDestinationHLayout->insertWidget(0, lNewFolderButton); } else if(!mSourceInfo.mIsDirectory && mFileWidget == nullptr) { - QFileInfo lFileInfo(mSourceInfo.mPathInRepo); - do { - lFileInfo.setFile(lFileInfo.absolutePath()); // check the file's directory first, not the file. - } while(!lFileInfo.exists()); - QUrl lStartSelection = QUrl::fromLocalFile(lFileInfo.absoluteFilePath() + '/' + mSourceFileName); - mFileWidget = new KFileWidget(lStartSelection, this); + if (mBackupType == BackupType::B_T_BUP) { + QFileInfo lFileInfo(mSourceInfo.mPathInRepo); + do { + lFileInfo.setFile(lFileInfo.absolutePath()); // check the file's directory first, not the file. + } while(!lFileInfo.exists()); + QUrl lStartSelection = QUrl::fromLocalFile(lFileInfo.absoluteFilePath() + '/' + mSourceFileName); + mFileWidget = new KFileWidget(lStartSelection, this); + } + else + mFileWidget = new KFileWidget(QUrl::fromLocalFile(QDir::home().absolutePath()), this); + mFileWidget->setOperationMode(KFileWidget::Saving); mFileWidget->setMode(KFile::File | KFile::LocalOnly); mUI->mDestinationVLayout->insertWidget(0, mFileWidget); diff --git a/filedigger/restoredialog.h b/filedigger/restoredialog.h index 34334fd..62dc7db 100644 --- a/filedigger/restoredialog.h +++ b/filedigger/restoredialog.h @@ -22,6 +22,7 @@ #define RESTOREDIALOG_H #include "versionlistmodel.h" +#include "vfshelpers.h" #include #include @@ -43,7 +44,7 @@ class RestoreDialog : public QDialog Q_OBJECT public: - explicit RestoreDialog(const BupSourceInfo &pPathInfo, QWidget *parent = 0); + explicit RestoreDialog(BackupType backupType, const BupSourceInfo &pPathInfo, QWidget *parent = 0); ~RestoreDialog(); protected: @@ -85,6 +86,7 @@ protected slots: QHash mFileSizes; int mDirectoriesCount; KWidgetJobTracker *mJobTracker; + BackupType mBackupType; }; #endif // RESTOREDIALOG_H diff --git a/filedigger/versionlistdelegate.cpp b/filedigger/versionlistdelegate.cpp index ad9ab46..3317014 100644 --- a/filedigger/versionlistdelegate.cpp +++ b/filedigger/versionlistdelegate.cpp @@ -136,6 +136,8 @@ VersionItemAnimation::VersionItemAnimation(QWidget *pParent) connect(mOpenButton, SIGNAL(focusChangeRequested(bool)), SLOT(changeFocus(bool)), Qt::QueuedConnection); mRestoreButton = new Button(xi18nc("@action:button", "Restore"), pParent); connect(mRestoreButton, SIGNAL(focusChangeRequested(bool)), SLOT(changeFocus(bool)), Qt::QueuedConnection); + mCopyButton = new Button(xi18nc("@action:button", "Copy"), pParent); + connect(mCopyButton, SIGNAL(focusChangeRequested(bool)), SLOT(changeFocus(bool)), Qt::QueuedConnection); QPropertyAnimation *lHeightAnimation = new QPropertyAnimation(this, "extraHeight", this); lHeightAnimation->setStartValue(0.0); lHeightAnimation->setEndValue(1.0); @@ -163,6 +165,9 @@ void VersionItemAnimation::changeFocus(bool pForward) { } else if(sender() == mRestoreButton) { mOpenButton->mStyleOption.state |= QStyle::State_HasFocus; mParent->update(mOpenButton->mStyleOption.rect); + } else if (sender() == mCopyButton) { + mCopyButton->mStyleOption.state |= QStyle::State_HasFocus; + mParent->update(mCopyButton->mStyleOption.rect); } } @@ -170,16 +175,20 @@ void VersionItemAnimation::setFocus(bool pFocused) { if(!pFocused) { mOpenButton->mStyleOption.state &= ~QStyle::State_HasFocus; mRestoreButton->mStyleOption.state &= ~QStyle::State_HasFocus; + mCopyButton->mStyleOption.state &= ~QStyle::State_HasFocus; } else { mOpenButton->mStyleOption.state |= QStyle::State_HasFocus; mRestoreButton->mStyleOption.state &= ~QStyle::State_HasFocus; + mCopyButton->mStyleOption.state &= ~QStyle::State_HasFocus; } mParent->update(mOpenButton->mStyleOption.rect); mParent->update(mRestoreButton->mStyleOption.rect); + mParent->update(mCopyButton->mStyleOption.rect); } -VersionListDelegate::VersionListDelegate(QAbstractItemView *pItemView, QObject *pParent) : +VersionListDelegate::VersionListDelegate(BackupType backupType, QAbstractItemView *pItemView, QObject *pParent) : QAbstractItemDelegate(pParent) + , mBackupType(backupType) { mView = pItemView; mModel = pItemView->model(); @@ -217,14 +226,19 @@ void VersionListDelegate::paint(QPainter *pPainter, const QStyleOptionViewItem & pPainter->save(); pPainter->setClipRect(pOption.rect); - lAnimation->mRestoreButton->setPosition(pOption.rect.topRight() + - QPoint(-cMargin, - pOption.fontMetrics.height() + 2*cMargin)); + lAnimation->mCopyButton->setPosition(pOption.rect.topRight() + + QPoint(-cMargin, + pOption.fontMetrics.height() + 2*cMargin)); + lAnimation->mCopyButton->paint(pPainter, lAnimation->opacity()); + + lAnimation->mRestoreButton->setPosition(lAnimation->mCopyButton->mStyleOption.rect.topLeft() + + QPoint(-cMargin , 0)); lAnimation->mRestoreButton->paint(pPainter, lAnimation->opacity()); lAnimation->mOpenButton->setPosition(lAnimation->mRestoreButton->mStyleOption.rect.topLeft() + - QPoint(-cMargin , 0)); + QPoint(-cMargin, 0)); lAnimation->mOpenButton->paint(pPainter, lAnimation->opacity()); + pPainter->restore(); } } @@ -237,9 +251,10 @@ QSize VersionListDelegate::sizeHint(const QStyleOptionViewItem &pOption, const Q int lButtonHeight = lAnimation->mOpenButton->mStyleOption.rect.height(); lExtraHeight = lAnimation->extraHeight() * (lButtonHeight + cMargin); lExtraWidth = lAnimation->mOpenButton->mStyleOption.rect.width() + - lAnimation->mRestoreButton->mStyleOption.rect.width(); + lAnimation->mRestoreButton->mStyleOption.rect.width() + + lAnimation->mCopyButton->mStyleOption.rect.width(); } - return QSize(lExtraWidth, cMargin*2 + pOption.fontMetrics.height() + lExtraHeight); + return QSize(lExtraWidth, cMargin*3 + pOption.fontMetrics.height() + lExtraHeight); } bool VersionListDelegate::eventFilter(QObject *pObject, QEvent *pEvent) { @@ -250,6 +265,9 @@ bool VersionListDelegate::eventFilter(QObject *pObject, QEvent *pEvent) { if(lAnimation->mRestoreButton->event(pEvent)) { emit restoreRequested(lAnimation->mIndex); } + if (lAnimation->mCopyButton->event(pEvent)) { + emit copyRequested(lAnimation->mIndex); + } } return QAbstractItemDelegate::eventFilter(pObject, pEvent); } diff --git a/filedigger/versionlistdelegate.h b/filedigger/versionlistdelegate.h index 76431e1..30988dc 100644 --- a/filedigger/versionlistdelegate.h +++ b/filedigger/versionlistdelegate.h @@ -25,6 +25,7 @@ #include #include +#include "vfshelpers.h" class Button : public QObject { Q_OBJECT @@ -69,6 +70,7 @@ public slots: float mOpacity; Button *mOpenButton; Button *mRestoreButton; + Button *mCopyButton; QWidget *mParent; }; @@ -76,7 +78,7 @@ class VersionListDelegate : public QAbstractItemDelegate { Q_OBJECT public: - explicit VersionListDelegate(QAbstractItemView *pItemView, QObject *pParent = 0); + explicit VersionListDelegate(BackupType pBackupType, QAbstractItemView *pItemView, QObject *pParent = 0); ~VersionListDelegate(); virtual void paint(QPainter *pPainter, const QStyleOptionViewItem &pOption, const QModelIndex &pIndex) const; virtual QSize sizeHint(const QStyleOptionViewItem &pOption, const QModelIndex &pIndex) const; @@ -85,6 +87,7 @@ class VersionListDelegate : public QAbstractItemDelegate signals: void openRequested(const QModelIndex &pIndex); void restoreRequested(const QModelIndex &pIndex); + void copyRequested(const QModelIndex &pIndex); public slots: void updateCurrent(const QModelIndex &pCurrent, const QModelIndex &pPrevious); @@ -97,6 +100,7 @@ public slots: QAbstractItemModel *mModel; QHash mActiveAnimations; QList mInactiveAnimations; + BackupType mBackupType; }; #endif // VERSIONDELEGATE_H diff --git a/filedigger/versionlistmodel.cpp b/filedigger/versionlistmodel.cpp index fcf95f1..1cb0aa0 100644 --- a/filedigger/versionlistmodel.cpp +++ b/filedigger/versionlistmodel.cpp @@ -60,9 +60,9 @@ QVariant VersionListModel::data(const QModelIndex &pIndex, int pRole) const { switch (pRole) { case Qt::DisplayRole: return lFormat.formatRelativeDateTime(QDateTime::fromTime_t(lData->mModifiedDate), QLocale::ShortFormat); - case VersionBupUrlRole: { + case VersionUrlRole: { QUrl lUrl; - mNode->getBupUrl(pIndex.row(), &lUrl); + mNode->getUrl(pIndex.row(), &lUrl); return lUrl; } case VersionMimeTypeRole: @@ -74,7 +74,7 @@ QVariant VersionListModel::data(const QModelIndex &pIndex, int pRole) const { return lData->size(); case VersionSourceInfoRole: { BupSourceInfo lSourceInfo; - mNode->getBupUrl(pIndex.row(), &lSourceInfo.mBupKioPath, &lSourceInfo.mRepoPath, &lSourceInfo.mBranchName, + mNode->getUrl(pIndex.row(), &lSourceInfo.mBupKioPath, &lSourceInfo.mRepoPath, &lSourceInfo.mBranchName, &lSourceInfo.mCommitTime, &lSourceInfo.mPathInRepo); lSourceInfo.mIsDirectory = mNode->isDirectory(); lSourceInfo.mSize = lData->size(); diff --git a/filedigger/versionlistmodel.h b/filedigger/versionlistmodel.h index e752244..5bef683 100644 --- a/filedigger/versionlistmodel.h +++ b/filedigger/versionlistmodel.h @@ -51,7 +51,7 @@ class VersionListModel : public QAbstractListModel }; enum VersionDataRole { - VersionBupUrlRole = Qt::UserRole + 1, // QUrl + VersionUrlRole = Qt::UserRole + 1, // QUrl VersionMimeTypeRole, // QString VersionSizeRole, // quint64 VersionSourceInfoRole, // PathInfo diff --git a/kcm/CMakeLists.txt b/kcm/CMakeLists.txt index ff4855f..b119796 100644 --- a/kcm/CMakeLists.txt +++ b/kcm/CMakeLists.txt @@ -22,6 +22,7 @@ add_library(kcm_kup MODULE ${kcm_kup_SRCS}) add_definitions(-DTRANSLATION_DOMAIN="kup") target_link_libraries(kcm_kup +resticcore Qt5::Core Qt5::DBus Qt5::Gui diff --git a/kcm/backupplanwidget.cpp b/kcm/backupplanwidget.cpp index 8f30930..4cccba1 100644 --- a/kcm/backupplanwidget.cpp +++ b/kcm/backupplanwidget.cpp @@ -39,6 +39,8 @@ #include #include #include +#include +#include #include #include @@ -469,6 +471,7 @@ QUrl DirDialog::url() const { BackupPlanWidget::BackupPlanWidget(BackupPlan *pBackupPlan, const QString &pBupVersion, + const QString &pResticVersion, const QString &pRsyncVersion, bool pPar2Available) : QWidget(), mBackupPlan(pBackupPlan) { @@ -482,7 +485,7 @@ BackupPlanWidget::BackupPlanWidget(BackupPlan *pBackupPlan, const QString &pBupV connect(mConfigureButton, SIGNAL(clicked()), this, SIGNAL(requestOverviewReturn())); mConfigPages = new KPageWidget; - mConfigPages->addPage(createTypePage(pBupVersion, pRsyncVersion)); + mConfigPages->addPage(createTypePage(pBupVersion, pResticVersion, pRsyncVersion)); mConfigPages->addPage(createSourcePage()); mConfigPages->addPage(createDestinationPage()); mConfigPages->addPage(createSchedulePage()); @@ -505,7 +508,9 @@ void BackupPlanWidget::saveExtraData() { mDriveSelection->saveExtraData(); } -KPageWidgetItem *BackupPlanWidget::createTypePage(const QString &pBupVersion, const QString &pRsyncVersion) { +KPageWidgetItem *BackupPlanWidget::createTypePage(const QString &pBupVersion, + const QString &pResticVersion, + const QString &pRsyncVersion) { mVersionedRadio = new QRadioButton; QString lVersionedInfo = xi18nc("@info", "This type of backup is an archive. It contains both " "the latest version of your files and earlier backed up versions. " @@ -552,6 +557,24 @@ KPageWidgetItem *BackupPlanWidget::createTypePage(const QString &pBupVersion, co } else { mSyncedRadio->setText(xi18nc("@option:radio", "Synchronized Backup")); } + + mResticRadio = new QRadioButton; + QString lResticInfo = xi18nc("@info", + "This type of backup is a versioned backup based on Restic."); + QLabel* lResticInfoLabel = new QLabel(lResticInfo); + lResticInfoLabel->setWordWrap(true); + QWidget *lResticWidget = new QWidget; + lResticWidget->setVisible(false); + connect(mResticRadio, SIGNAL(toggled(bool)), lResticWidget, SLOT(setVisible(bool))); + if(pResticVersion.isEmpty()) { + mResticRadio->setText(xi18nc("@option:radio", "Versioned backup based on Restic (not available " + "because Restic is not installed)")); + mResticRadio->setEnabled(false); + lResticWidget->setEnabled(false); + } else { + mResticRadio->setText(xi18nc("@option:radio", "Versioned backup based on Restic")); + } + KButtonGroup *lButtonGroup = new KButtonGroup; lButtonGroup->setObjectName(QStringLiteral("kcfg_Backup type")); lButtonGroup->setFlat(true); @@ -570,13 +593,22 @@ KPageWidgetItem *BackupPlanWidget::createTypePage(const QString &pBupVersion, co lSyncedVLayout->addWidget(lSyncedInfoLabel, 0, 1); lSyncedWidget->setLayout(lSyncedVLayout); + QGridLayout *lResticVLayout = new QGridLayout; + lResticVLayout->setColumnMinimumWidth(0, lIndentation); + lResticVLayout->setContentsMargins(0, 0, 0, 0); + lResticVLayout->addWidget(lResticInfoLabel, 0, 1); + lResticWidget->setLayout(lResticVLayout); + QVBoxLayout *lVLayout = new QVBoxLayout; lVLayout->addWidget(mVersionedRadio); lVLayout->addWidget(lVersionedWidget); lVLayout->addWidget(mSyncedRadio); lVLayout->addWidget(lSyncedWidget); + lVLayout->addWidget(mResticRadio); + lVLayout->addWidget(lResticWidget); lVLayout->addStretch(); lButtonGroup->setLayout(lVLayout); + KPageWidgetItem *lPage = new KPageWidgetItem(lButtonGroup); lPage->setName(xi18nc("@title", "Backup Type")); lPage->setHeader(xi18nc("@label", "Select what type of backup you want")); @@ -872,12 +904,19 @@ KPageWidgetItem *BackupPlanWidget::createAdvancedPage(bool pPar2Available) { lVerificationWidget->setLayout(lVerificationLayout); connect(mVersionedRadio, SIGNAL(toggled(bool)), lVerificationWidget, SLOT(setVisible(bool))); + QWidget *removalWidget = createRemovalGroup(lIndentation); + removalWidget->setVisible(mResticRadio->isChecked()); + connect(mResticRadio, SIGNAL(toggled(bool)), + removalWidget, SLOT(setVisible(bool))); + lAdvancedLayout->addWidget(lShowHiddenCheckBox); lAdvancedLayout->addLayout(lShowHiddenLayout); lAdvancedLayout->addWidget(lVerificationWidget); lAdvancedLayout->addWidget(lRecoveryWidget); + lAdvancedLayout->addWidget(removalWidget); lAdvancedLayout->addStretch(); lAdvancedWidget->setLayout(lAdvancedLayout); + KPageWidgetItem *lPage = new KPageWidgetItem(lAdvancedWidget); lPage->setName(xi18nc("@title", "Advanced")); lPage->setHeader(xi18nc("@label", "Extra options for advanced users")); @@ -885,6 +924,206 @@ KPageWidgetItem *BackupPlanWidget::createAdvancedPage(bool pPar2Available) { return lPage; } +QWidget *BackupPlanWidget::createRemovalGroup(int pIndentation) +{ + // TODO: Remove this param if unneeded. + Q_UNUSED(pIndentation); + + QGroupBox *lGroup = new QGroupBox; + lGroup->setTitle(i18n("Removal options")); + + QLabel *lDescription = new QLabel(i18n("A versioned backup keeps older versions " + "of your files and stores also files that " + "were deleted. You can use these options " + "to prevent your backup from growing indefinitely.")); + lDescription->setWordWrap(true); + + // Keep last n. + QCheckBox *lKeepLastNCb; + QSpinBox *lKeepLastNSb; + createRemoveRow(&lKeepLastNCb, "Always keep the last n snapshots:", + &lKeepLastNSb, mResticRadio->isChecked(), 5, + QStringLiteral("kcfg_Keep last n"), + i18n("Never delete the last (more recent) n snapshots.")); + + // Keep hourly. + QCheckBox *lKeepHourlyCb; + QSpinBox *lKeepHourlySb; + createRemoveRow(&lKeepHourlyCb, "Keep hourly for last n hours:", + &lKeepHourlySb, mResticRadio->isChecked(), 5, + QStringLiteral("kcfg_Keep hourly"), + i18n("For the last n hours in which a snapshot was " + "made, keep only the last snapshot for each hour.")); + + // Keep daily. + QCheckBox *lKeepDailyCb; + QSpinBox *lKeepDailySb; + createRemoveRow(&lKeepDailyCb, "Keep daily for last n days:", + &lKeepDailySb, mResticRadio->isChecked(), 5, + QStringLiteral("kcfg_Keep daily"), + i18n("For the last n days in which a snapshot was " + "made, keep only the last snapshot for that day.")); + + // Keep monthly. + QCheckBox *lKeepMonthlyCb; + QSpinBox *lKeepMonthlySb; + createRemoveRow(&lKeepMonthlyCb, "Keep monthly for last n months:", + &lKeepMonthlySb, mResticRadio->isChecked(), 5, + QStringLiteral("kcfg_Keep monthly"), + i18n("For the last n months in which a snapshot was " + "made, keep only the last snapshot for each month.")); + + // Keep yearly. + QCheckBox *lKeepYearlyCb; + QSpinBox *lKeepYearlySb; + createRemoveRow(&lKeepYearlyCb, "Keep yearly for the last n years:", + &lKeepYearlySb, mResticRadio->isChecked(), 5, + QStringLiteral("kcfg_Keep yearly"), + i18n("For the last n years in which a snapshot was " + "made, keep only the last snapshot for each year.")); + + // Keep within duration. + QString lKeepWithinDurationTT = i18n("Keep all snapshots which " + "have been made within a specified " + "duration from the latest snapshot."); + QCheckBox *lKeepWithinDuration = new QCheckBox(xi18nc("@option:check", + "Keep all snapshots within duration:")); + lKeepWithinDuration->setObjectName(QStringLiteral("kcfg_Keep within duration")); + lKeepWithinDuration->setToolTip(lKeepWithinDurationTT); + + mDurationYears = new QSpinBox; + mDurationYears->setEnabled(mResticRadio->isChecked()); + mDurationYears->setValue(1); + mDurationYears->setObjectName(QStringLiteral("kcfg_Keep within duration years")); + mDurationYears->setToolTip(lKeepWithinDurationTT); + connect(lKeepWithinDuration, SIGNAL(toggled(bool)), + mDurationYears, SLOT(setEnabled(bool))); + connect(mDurationYears, SIGNAL(valueChanged(int)), + this, SLOT(refreshDuration())); + + mDurationMonths = new QSpinBox; + mDurationMonths->setEnabled(mResticRadio->isChecked()); + mDurationMonths->setValue(0); + mDurationMonths->setObjectName(QStringLiteral("kcfg_Keep within duration months")); + mDurationMonths->setToolTip(lKeepWithinDurationTT); + connect(lKeepWithinDuration, SIGNAL(toggled(bool)), + mDurationMonths, SLOT(setEnabled(bool))); + connect(mDurationMonths, SIGNAL(valueChanged(int)), + this, SLOT(refreshDuration())); + + mDurationDays = new QSpinBox; + mDurationDays->setEnabled(mResticRadio->isChecked()); + mDurationDays->setValue(0); + mDurationDays->setObjectName(QStringLiteral("kcfg_Keep within duration days")); + mDurationDays->setToolTip(lKeepWithinDurationTT); + connect(lKeepWithinDuration, SIGNAL(toggled(bool)), + mDurationDays, SLOT(setEnabled(bool))); + connect(mDurationDays, SIGNAL(valueChanged(int)), + this, SLOT(refreshDuration())); + + QHBoxLayout *lYearsLayout = new QHBoxLayout; + lYearsLayout->addWidget(mDurationYears); + lYearsLayout->addWidget(new QLabel(i18n("years"))); + + QHBoxLayout *lMonthsLayout = new QHBoxLayout; + lMonthsLayout->addWidget(mDurationMonths); + lMonthsLayout->addWidget(new QLabel(i18n("months"))); + + QHBoxLayout *lDaysLayout = new QHBoxLayout; + lDaysLayout->addWidget(mDurationDays); + lDaysLayout->addWidget(new QLabel(i18n("days"))); + + // Build summary. + mDurationSummary = new QLabel; + mDurationSummary->setEnabled(mResticRadio->isChecked()); + connect(lKeepWithinDuration, SIGNAL(toggled(bool)), + mDurationSummary, SLOT(setEnabled(bool))); + + refreshDuration(); + + // Separator. + QFrame* f = new QFrame; + f->setFrameStyle(QFrame::Plain); + f->setFrameShape(QFrame::HLine); + + QFormLayout *layout = new QFormLayout; + layout->setContentsMargins(20, 20, 20, 20); + layout->setSpacing(20); + layout->addRow(lDescription); + layout->addRow(f); + layout->addRow(lKeepLastNCb, lKeepLastNSb); + layout->addRow(lKeepHourlyCb, lKeepHourlySb); + layout->addRow(lKeepDailyCb, lKeepDailySb); + layout->addRow(lKeepWithinDuration, lYearsLayout); + layout->addRow(new QWidget, lMonthsLayout); + layout->addRow(new QWidget, lDaysLayout); + layout->addRow(mDurationSummary); + + lGroup->setLayout(layout); + + return lGroup; +} + +void BackupPlanWidget::createRemoveRow(QCheckBox **cb, + const char *label, + QSpinBox **sb, + bool cbValue, + int sbValue, + const QString &kcfgLabel, + const QString &tooltip) +{ + (*cb) = new QCheckBox(xi18nc("@option:check", label)); + (*cb)->setChecked(cbValue); + (*cb)->setObjectName(kcfgLabel); + (*cb)->setToolTip(tooltip); + + (*sb) = new QSpinBox; + (*sb)->setValue(sbValue); + (*sb)->setEnabled(false); + (*sb)->setObjectName(QString("%1 value").arg(kcfgLabel)); + (*sb)->setToolTip(tooltip); + connect(*cb, SIGNAL(toggled(bool)), + *sb, SLOT(setEnabled(bool))); +} + +void BackupPlanWidget::refreshDuration() +{ + // Build summary. + QString summary; + if (mDurationYears->value() > 0 + && mDurationMonths->value() > 0 + && mDurationDays->value() > 0) + summary = i18n("All snapshots within %1 year(s), %2 month(s) and %3 day(s) will be preserved.", + mDurationYears->value(), + mDurationMonths->value(), + mDurationDays->value()); + else if (mDurationYears->value() > 0 && mDurationMonths->value() > 0) + summary = i18n("All snapshots within %1 year(s) and %2 month(s) will be preserved.", + mDurationYears->value(), + mDurationMonths->value()); + else if (mDurationYears->value() > 0 && mDurationDays->value() > 0) + summary = i18n("All snapshots within %1 year(s) and %2 day(s) will be preserved.", + mDurationYears->value(), + mDurationDays->value()); + else if (mDurationMonths->value() > 0 && mDurationDays->value() > 0) + summary = i18n("All snapshots within %1 month(s) and %2 day(s) will be preserved.", + mDurationMonths->value(), + mDurationDays->value()); + else if (mDurationYears->value() > 0) + summary = i18n("All snapshots within %1 year(s) will be preserved.", + mDurationYears->value()); + else if (mDurationMonths->value() > 0) + summary = i18n("All snapshots within %1 month(s) will be preserved.", + mDurationMonths->value()); + else if (mDurationDays->value() > 0) + summary = i18n("All snapshots within %1 day(s) will be preserved.", + mDurationDays->value()); + else + summary = QString(); + + mDurationSummary->setText(summary); +} + void BackupPlanWidget::openDriveDestDialog() { QString lMountPoint = mDriveSelection->mountPathOfSelectedDrive(); QString lSelectedPath; diff --git a/kcm/backupplanwidget.h b/kcm/backupplanwidget.h index 3255183..df2a703 100644 --- a/kcm/backupplanwidget.h +++ b/kcm/backupplanwidget.h @@ -41,6 +41,9 @@ class QRadioButton; class QThread; class QTimer; class QTreeView; +class QCheckBox; +class QSpinBox; +class QLabel; class FileScanner : public QObject { Q_OBJECT @@ -148,16 +151,31 @@ class BackupPlanWidget : public QWidget Q_OBJECT public: BackupPlanWidget(BackupPlan *pBackupPlan, const QString &pBupVersion, + const QString &pResticVersion, const QString &pRsyncVersion, bool pPar2Available); void saveExtraData(); - KPageWidgetItem *createTypePage(const QString &pBupVersion, const QString &pRsyncVersion); + KPageWidgetItem *createTypePage(const QString &pBupVersion, + const QString &pResticVersion, const QString &pRsyncVersion); KPageWidgetItem *createSourcePage(); KPageWidgetItem *createDestinationPage(); KPageWidgetItem *createSchedulePage(); KPageWidgetItem *createAdvancedPage(bool pPar2Available); + QWidget *createRemovalGroup(int pIndentation); + void createRemoveRow(QCheckBox **cb, + const char *label, + QSpinBox **sb, + bool cbValue, + int sbValue, + const QString &kcfgLabel, + const QString &tooltip); + +public slots: + void refreshDuration(); + +public: KLineEdit *mDescriptionEdit; QPushButton *mConfigureButton; KPageWidget *mConfigPages; @@ -166,8 +184,15 @@ class BackupPlanWidget : public QWidget KLineEdit *mDriveDestEdit; QRadioButton *mVersionedRadio; QRadioButton *mSyncedRadio; + QRadioButton *mResticRadio; FolderSelectionWidget *mSourceSelectionWidget; + // Restic. + QSpinBox *mDurationYears; + QSpinBox *mDurationMonths; + QSpinBox *mDurationDays; + QLabel *mDurationSummary; + protected slots: void openDriveDestDialog(); diff --git a/kcm/kupkcm.cpp b/kcm/kupkcm.cpp index 54c75e9..f8661ae 100644 --- a/kcm/kupkcm.cpp +++ b/kcm/kupkcm.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include @@ -41,6 +42,8 @@ #include #include +#include + K_PLUGIN_FACTORY(KupKcmFactory, registerPlugin();) KupKcm::KupKcm(QWidget *pParent, const QVariantList &pArgs) @@ -81,14 +84,32 @@ KupKcm::KupKcm(QWidget *pParent, const QVariantList &pArgs) mRsyncVersion = lOutput.split(QLatin1Char(' '), QString::SkipEmptyParts).at(2); } - if(mBupVersion.isEmpty() && mRsyncVersion.isEmpty()) { + QVersionNumber resticVersion = ResticHelper::version(); + if (!resticVersion.isNull()) + mResticVersion = resticVersion.toString(); + + KProcess lResticProcess; + lResticProcess << QStringLiteral("restic") << QStringLiteral("version"); + lResticProcess.setOutputChannelMode(KProcess::MergedChannels); + lExitCode = lResticProcess.execute(); + if(lExitCode >= 0) { + QString lOutput = QString::fromLocal8Bit(lResticProcess.readLine()); + QStringList tokens = lOutput.split(QLatin1Char(' '), QString::SkipEmptyParts); + QVersionNumber version = QVersionNumber::fromString(tokens[1]); + if (version.minorVersion() == 8 || version.minorVersion() == 9) + mResticVersion = tokens[1]; + } + + if(mBupVersion.isEmpty() && mRsyncVersion.isEmpty() && mResticVersion.isEmpty()) { QLabel *lSorryIcon = new QLabel; lSorryIcon->setPixmap(QIcon::fromTheme(QStringLiteral("dialog-error")).pixmap(64, 64)); QString lInstallMessage = i18n("

Backup programs are missing

" "

Before you can activate any backup plan you need to " "install either of

" "
  • bup, for versioned backups
  • " - "
  • rsync, for synchronized backups
"); + "
  • restic, for versioned backups
  • " + "
  • rsync, for synchronized backups
  • " + ""); QLabel *lSorryText = new QLabel(lInstallMessage); lSorryText->setWordWrap(true); QHBoxLayout *lHLayout = new QHBoxLayout; @@ -122,7 +143,7 @@ QSize KupKcm::sizeHint() const { } void KupKcm::load() { - if(mBupVersion.isEmpty() && mRsyncVersion.isEmpty()) { + if(mBupVersion.isEmpty() && mRsyncVersion.isEmpty() && mResticVersion.isEmpty()) { return; } // status will be set correctly after construction, set to checked here to @@ -252,6 +273,7 @@ void KupKcm::createSettingsFrontPage() { void KupKcm::createPlanWidgets(int pIndex) { BackupPlanWidget *lPlanWidget = new BackupPlanWidget(mPlans.at(pIndex), mBupVersion, + mResticVersion, mRsyncVersion, mPar2Available); connect(lPlanWidget, SIGNAL(requestOverviewReturn()), this, SLOT(showFrontPage())); KConfigDialogManager *lConfigManager = new KConfigDialogManager(lPlanWidget, mPlans.at(pIndex)); diff --git a/kcm/kupkcm.h b/kcm/kupkcm.h index 7241804..2f921ae 100644 --- a/kcm/kupkcm.h +++ b/kcm/kupkcm.h @@ -70,6 +70,7 @@ public slots: QCheckBox *mEnableCheckBox; QString mBupVersion; QString mRsyncVersion; + QString mResticVersion; bool mPar2Available; }; diff --git a/kioslave/vfshelpers.h b/kioslave/vfshelpers.h index 25f1b0e..8223205 100644 --- a/kioslave/vfshelpers.h +++ b/kioslave/vfshelpers.h @@ -29,6 +29,11 @@ class QBuffer; #define DEFAULT_MODE_DIRECTORY 0040755 #define DEFAULT_MODE_FILE 0100644 +enum BackupType { + B_T_BUP, + B_T_RESTIC +}; + class VintStream: public QObject { Q_OBJECT diff --git a/regex b/regex new file mode 100644 index 0000000..e458144 --- /dev/null +++ b/regex @@ -0,0 +1 @@ +\[\d*:\d*\]\s*([\d\.]+)%\s+\d+\s*\/\s*\d+\s+packs diff --git a/resticcore/CMakeLists.txt b/resticcore/CMakeLists.txt new file mode 100644 index 0000000..45b1f74 --- /dev/null +++ b/resticcore/CMakeLists.txt @@ -0,0 +1,21 @@ +set(resticcore_SRCS +restichelper.cpp +resticforgetswitch.cpp +) + +ecm_qt_declare_logging_category(resticcore_SRCS + HEADER kupresticcore_debug.h + IDENTIFIER KUPRESTICCORE + CATEGORY_NAME kup.resticcore + DEFAULT_SEVERITY Debug +) + +add_definitions(-fexceptions) + +add_library(resticcore STATIC ${resticcore_SRCS}) +target_link_libraries(resticcore +Qt5::Core +KF5::CoreAddons +KF5::Pty +KF5::I18n +) diff --git a/resticcore/resticforgetswitch.cpp b/resticcore/resticforgetswitch.cpp new file mode 100644 index 0000000..cb7f745 --- /dev/null +++ b/resticcore/resticforgetswitch.cpp @@ -0,0 +1,30 @@ +/*************************************************************************** + * Copyright Luca Carlon * + * carlon.luca@gmail.com * + * * + * 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., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#include "resticforgetswitch.h" + +#define QSL QStringLiteral + +ResticForgetSwitch::ResticForgetSwitch(const QString ¶m, const QString &value) : + param(param) + , value(value) +{ + // Do nothing. +} diff --git a/resticcore/resticforgetswitch.h b/resticcore/resticforgetswitch.h new file mode 100644 index 0000000..0f16df3 --- /dev/null +++ b/resticcore/resticforgetswitch.h @@ -0,0 +1,89 @@ +/*************************************************************************** + * Copyright Luca Carlon * + * carlon.luca@gmail.com * + * * + * 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., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#ifndef RESTICFORGETSWITCH_H +#define RESTICFORGETSWITCH_H + +#include + +class ResticForgetSwitch +{ +public: + ResticForgetSwitch(const QString ¶m, const QString &value); + + QString param; + QString value; +}; + +#define QSL QStringLiteral + +class ResticForgetKeepLastN : public ResticForgetSwitch +{ +public: + ResticForgetKeepLastN(int value) : + ResticForgetSwitch(QSL("--keep-last"), QString::number(value)) {} +}; + +class ResticForgetKeepHourly : public ResticForgetSwitch +{ +public: + ResticForgetKeepHourly(int value) : + ResticForgetSwitch(QSL("--keep-hourly"), QString::number(value)) {} +}; + +class ResticForgetKeepDaily : public ResticForgetSwitch +{ +public: + ResticForgetKeepDaily(int value) : + ResticForgetSwitch(QSL("--keep-daily"), QString::number(value)) {} +}; + +class ResticForgetKeepWeekly : public ResticForgetSwitch +{ +public: + ResticForgetKeepWeekly(int value) : + ResticForgetSwitch(QSL("--keep-weekly"), QString::number(value)) {} +}; + +class ResticForgetKeepMonthly : public ResticForgetSwitch +{ +public: + ResticForgetKeepMonthly(int value) : + ResticForgetSwitch(QSL("--keep-monthly"), QString::number(value)) {} +}; + +class ResticForgetKeepYearly : public ResticForgetSwitch +{ +public: + ResticForgetKeepYearly(int value) : + ResticForgetSwitch(QSL("--keep-yearly"), QString::number(value)) {} +}; + +class ResticForgetKeepWithinDuration : public ResticForgetSwitch +{ +public: + ResticForgetKeepWithinDuration(int years, int months, int days) : + ResticForgetSwitch(QSL("--keep-within"), + QString("%1y%2m%3d").arg(years).arg(months).arg(days)) {} +}; + +#undef QSL + +#endif // RESTICFORGETSWITCH_H diff --git a/resticcore/restichelper.cpp b/resticcore/restichelper.cpp new file mode 100644 index 0000000..4d9fe26 --- /dev/null +++ b/resticcore/restichelper.cpp @@ -0,0 +1,568 @@ +/*************************************************************************** + * Copyright Luca Carlon * + * carlon.luca@gmail.com * + * * + * 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., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include "restichelper.h" +#include "kupresticcore_debug.h" + +inline double to_double_or_null(const QString &value) +{ + bool convOk = false; + double val = value.toDouble(&convOk); + return convOk ? val : 0; +} + +inline qint64 to_longlong_or_null(const QString &value) +{ + bool convOk = false; + qint64 val = value.toLongLong(&convOk); + return convOk ? val : 0; +} + +ResticProcess::ResticProcess(const QString &repoPath) : KPtyProcess() +{ + setOutputChannelMode(KProcess::MergedChannels); + setPtyChannels(KPtyProcess::StdoutChannel); + + if (pty()->masterFd() == -1) + qCWarning(KUPRESTICCORE) << "Master fd not set"; + + setEnv("RESTIC_PASSWORD", "kup"); + *this << RESTIC_BIN << QSL("-r") << repoPath; +} + +bool MountLock::umountMountPoint() +{ + // Try to unmount. If it fails, then report failure and do nothing else. + return QProcess::execute(QSL("fusermount"), QStringList() << "-u" << mMountPath) == 0; +} + +bool MountLock::removeMountPoint() +{ + // Remove mountpoint. + if (!QDir().rmdir(mMountPath)) { + qCWarning(KUPRESTICCORE) << "Failed to remove mountpoint " << mMountPath; + return false; + } + + return true; +} + +ResticMountLock::ResticMountLock(const QString &repoPath, const QString &mountPath) + : MountLock(mountPath) + , mProcess(repoPath) +{ + connect(mProcess.pty(), SIGNAL(readyRead()), + this, SLOT(onStdoutData())); + + qCDebug(KUPRESTICCORE) << "Mouting to " << mountPath; + mProcess << QSL("mount") << mountPath; + mProcess.start(); +} + +bool ResticMountLock::isLocked() +{ + return mProcess.state() != KPtyProcess::NotRunning; +} + +bool ResticMountLock::unlock() +{ + umountMountPoint(); + + // At this point this process should already be dead, but terminate + // anyway for safety. SIGINT seems to be needed to also include unmount. + ::kill(mProcess.pid(), SIGINT); + if (!mProcess.waitForFinished()) { + qCWarning(KUPRESTICCORE) << "The restic process seems to be stuck"; + + // I don't return false here as the process seems to be stuck, but umount + // reported success. + } + + removeMountPoint(); + + return true; +} + +void ResticMountLock::onStdoutData() +{ + QByteArray data = mProcess.pty()->readAll(); + QString string = QString::fromUtf8(data); + mOutput.append(string); + + qCWarning(KUPRESTICCORE) << "Data: " << mOutput; + + if (mOutput.contains("serving the repository at")) { + emit servingData(); + mOutput.clear(); + } +} + +ResticProgressProcess::ResticProgressProcess(const QString &repoPath) : + ResticProcess(repoPath) +{ + connect(pty(), SIGNAL(readyRead()), + this, SLOT(onStdoutData())); + connect(this, SIGNAL(finished(int,QProcess::ExitStatus)), + this, SLOT(onFinished(int,QProcess::ExitStatus))); +} + +void ResticProgressProcess::onStdoutData() +{ + QTextStream stream(pty()->readAll()); + while (!stream.atEnd()) { + QString line = stream.readLine(); + ResticProgressState state; + if (!processStepLine(line, state)) + continue; + emitStepSignal(state); + } +} + +void ResticProgressProcess::onFinished(int errorCode, QProcess::ExitStatus exitStatus) +{ + if (exitStatus == QProcess::CrashExit) { + qCWarning(KUPRESTICCORE) << "Restic crashed"; + emitFailedSignal(); + return; + } + + if (!errorCode) { + qCInfo(KUPRESTICCORE) << "Restic completed process successfully"; + emitCompletedSignal(); + return; + } + + // TODO: Send error info. + qCWarning(KUPRESTICCORE) << "Restic failed procedure"; + emit emitFailedSignal(); +} + +ResticForgetProcess::ResticForgetProcess(const QString &repoPath, + const QList &switches) : + ResticProgressProcess(repoPath) + , m_regex("\\[\\d*:\\d*\\]\\s*([\\d\\.]+)%\\s+(\\d+)\\s*\\/\\s*(\\d+)\\s+packs") + , m_state(ResticProgressState::S_BUILDING_NEW_INDEX) +{ + (*this) << QSL("forget"); + for (int i = 0; i < switches.size(); i++) { + const ResticForgetSwitch &forgetSwitch = switches[i]; + (*this) << forgetSwitch.param << forgetSwitch.value; + } + + (*this) << QSL("--prune"); + + start(); + + ResticProgressState state; + state.mPerc = 0; + state.speedBps = 0; // TODO: compute speed. + state.processedItems = 0; + state.processedBytes = 0; + state.totalItems = 0; + state.totalBytes = 0; + state.state = ResticProgressState::S_BUILDING_NEW_INDEX; + + emit forgetStep(state); +} + +bool ResticForgetProcess::processStepLine(const QString &line, ResticProgressState &state) +{ + if (line.contains(QStringLiteral("running prune"))) { + state.mPerc = 0; + state.speedBps = 0; // TODO: compute speed. + state.processedItems = 0; + state.processedBytes = 0; + state.totalItems = 0; + state.totalBytes = 0; + state.state = ResticProgressState::S_BUILDING_NEW_INDEX; + return true; + } + + if (line.contains(QStringLiteral("building new index for repo"))) { + m_state = ResticProgressState::S_BUILDING_NEW_INDEX; + return true; + } + else if (line.contains("find data that is still in use")) { + m_state = ResticProgressState::S_FIND_DATA_IN_USE; + return true; + } + else if (line.contains("counting files in repo")) { + m_state = ResticProgressState::S_COUNTING; + return true; + } + else if (line.contains(QRegularExpression("remove [\\d]+ old index file"))) { + m_state = ResticProgressState::S_REMOVING; + return true; + } + + QRegularExpressionMatch match = m_regex.match(line); + if (!match.hasMatch()) + return false; + + qCDebug(KUPRESTICCORE) << "Step: " << match.captured(0); + if (match.lastCapturedIndex() != 3) + return false; + + QString sPerc = match.captured(1); + QString sProcessedItems = match.captured(2); + QString sTotalItems = match.captured(3); + + double perc = to_double_or_null(sPerc); + qCWarning(KUPRESTICCORE) << "Done: " << perc << sProcessedItems; + + state.mPerc = qRound(perc); + state.speedBps = 0; // TODO + state.processedItems = to_longlong_or_null(sProcessedItems); + state.processedBytes = 0; + state.totalItems = to_longlong_or_null(sTotalItems); + state.totalBytes = 0; + state.state = m_state; + + return true; +} + +ResticBackupProcess::ResticBackupProcess(const QString &repoPath, + const QStringList &includedPaths, + const QStringList &excludedPaths, + const QString ®ex) : + ResticProgressProcess(repoPath) + , mRegexTransfer(regex) +{ + (*this) << QSL("backup") << includedPaths; + + foreach (QString excludedPath, excludedPaths) + (*this) << QSL("-e") << excludedPath; + + start(); +} + +qint64 ResticBackupProcess::processMeasureToBytes(const QString &number, const QString &unit) +{ + bool ok = false; + double value = number.toDouble(&ok); + if (!ok) + return -1; + +#define KIB2B(b) qRound64(1024*b) +#define MIB2B(m) KIB2B(1024*m) +#define GIB2B(g) MIB2B(1024*g) + + if (unit == "GiB") + return GIB2B(value); + if (unit == "MiB") + return MIB2B(value); + if (unit == "KiB") + return KIB2B(value); + + qCWarning(KUPRESTICCORE) << "Unknown unit " << unit; + return 0; +} + +bool ResticBackupProcess08::processStepLine(const QString &line, ResticProgressState &state) +{ + if (line.startsWith("scan", Qt::CaseInsensitive)) { + state.mPerc = 0; + state.speedBps = 0; + state.processedItems = 0; + state.processedBytes = 0; + state.totalItems = 0; + state.totalBytes = 0; + state.state = ResticProgressState::S_SCANNING; + return true; + } + + QRegularExpressionMatch match = mRegexTransfer.match(line); + if (!match.hasMatch()) + return false; + + qCDebug(KUPRESTICCORE) << "Step: " << match.captured(0); + if (match.lastCapturedIndex() != 8) + return false; + + QString sPerc = match.captured(1); + QString sProcessed = match.captured(2); + QString sProcessedUnit = match.captured(3); + QString sTotal = match.captured(4); + QString sTotalUnit = match.captured(5); + QString sProcessedItems = match.captured(6); + QString sTotalItems = match.captured(7); + QString sErrors = match.captured(8); + + qint64 processed = processMeasureToBytes(sProcessed, sProcessedUnit); + qint64 total = processMeasureToBytes(sTotal, sTotalUnit); + double perc = to_double_or_null(sPerc); + + // TODO: Compute speed. + state.mPerc = qMin(100, qMax(0, qRound(perc))); + state.speedBps = 0; + state.processedItems = to_longlong_or_null(sProcessedItems); + state.processedBytes = processed; + state.totalItems = to_longlong_or_null(sTotalItems); + state.totalBytes = total; + state.state = ResticProgressState::S_TRANSFERING; + + return true; +} + +bool ResticBackupProcess09::processStepLine(const QString &line, ResticProgressState &state) +{ + if (line.startsWith("scan", Qt::CaseInsensitive)) { + state.mPerc = 0; + state.speedBps = 0; + state.processedItems = 0; + state.processedBytes = 0; + state.totalItems = 0; + state.totalBytes = 0; + state.state = ResticProgressState::S_SCANNING; + return true; + } + + QRegularExpressionMatch match = mRegexTransfer.match(line); + if (!match.hasMatch()) + return false; + + qCDebug(KUPRESTICCORE) << "Step: " << match.captured(0); + if (match.lastCapturedIndex() != 8) + return false; + + QString sPerc = match.captured(1); + QString sProcessed = match.captured(3); + QString sProcessedUnit = match.captured(4); + QString sTotal = match.captured(6); + QString sTotalUnit = match.captured(7); + QString sProcessedItems = match.captured(2); + QString sTotalItems = match.captured(5); + QString sErrors = match.captured(8); + + qint64 processed = processMeasureToBytes(sProcessed, sProcessedUnit); + qint64 total = processMeasureToBytes(sTotal, sTotalUnit); + double perc = to_double_or_null(sPerc); + + // TODO: Compute speed. + state.mPerc = qMin(100, qMax(0, qRound(perc))); + state.speedBps = 0; + state.processedItems = to_longlong_or_null(sProcessedItems); + state.processedBytes = processed; + state.totalItems = to_longlong_or_null(sTotalItems); + state.totalBytes = total; + state.state = ResticProgressState::S_TRANSFERING; + + return true; +} + +ResticHelper::ResticHelper() +{ + +} + +bool ResticHelper::repoExists(const QString &repo) +{ + ResticProcess proc(repo); + proc << QSL("snapshots"); + + return proc.execute() == 0; +} + +bool ResticHelper::repoInit(const QString &repo) +{ + ResticProcess proc(repo); + proc << QSL("init"); + + return proc.execute() == 0; +} + +bool ResticHelper::repoCheck(const QString &repo, QString &output) +{ + ResticProcess proc(repo); + proc.setOutputChannelMode(ResticProcess::MergedChannels); + proc << QSL("check"); + + int ret = (proc.execute() == 0); + output.clear(); + output.append(QString::fromUtf8(proc.readAllStandardOutput())); + + return ret; +} + +ResticBackupProcess *ResticHelper::repoBackup(const QString &repo, + const QStringList &includedPaths, + const QStringList &excludedPaths) +{ + return new ResticBackupProcess08(repo, includedPaths, excludedPaths); +} + +ResticMountLock *ResticHelper::repoMount(const QString &repo, const QString &mountPath) +{ + if (isRepoMounted(repo)) + return nullptr; + + return new ResticMountLock(repo, mountPath); +} + +ResticForgetProcess *ResticHelper::repoForget(const QString &repo, + const QList &switches) +{ + return new ResticForgetProcess(repo, switches); +} + +QStringList ResticHelper::resticMountPaths() +{ + QFile mounts("/proc/mounts"); + if (!mounts.exists()) { + qCWarning(KUPRESTICCORE) << "Cannot find /proc/mounts table"; + return QStringList(); + } + + if (!mounts.open(QIODevice::ReadOnly)) { + qCWarning(KUPRESTICCORE) << "Cannot open /proc/mounts for reading"; + return QStringList(); + } + + QStringList mountPaths; + QTextStream stream(&mounts); + QString line; + while (stream.readLineInto(&line)) { + QString line = stream.readLine(); + QStringList tokens = line.split(" "); + if (tokens.size() > 2) + if (tokens[0] == QSL("restic")) + mountPaths.append(tokens[1]); + } + + return mountPaths; +} + +QList ResticHelper::resticMountDirs() +{ + QList dirs; + QStringList paths = resticMountPaths(); + foreach (QString path, paths) + dirs.append(QDir(path)); + + return dirs; +} + +QVersionNumber ResticHelper::version() +{ + KProcess lResticProcess; + lResticProcess << QSL("restic") << QSL("version"); + lResticProcess.setOutputChannelMode(KProcess::MergedChannels); + int code = lResticProcess.execute(); + if (code >= 0) { + QString lOutput = QString::fromLocal8Bit(lResticProcess.readLine()); + QStringList tokens = lOutput.split(QLatin1Char(' '), QString::SkipEmptyParts); + return QVersionNumber::fromString(tokens[1]); + } + + return QVersionNumber(); +} + +bool ResticHelper::isRepoMounted(const QString &repo) +{ + QStringList mountPaths = resticMountPaths(); + for (int i = 0; i < mountPaths.size(); i++) + if (QDir(repo) == QDir(mountPaths[i])) + return true; + + return false; +} + +ResticMountLock *ResticMountManager::mount(const QString &repo) +{ + qCDebug(KUPRESTICCORE) << "Mounting..."; + + QString dataDirString = generateMountPrivateDir(); + if (dataDirString.isEmpty()) + return nullptr; + + QDir dataDir(dataDirString); + + qCDebug(KUPRESTICCORE) << "Created temporary mountpoint " << dataDir.absolutePath(); + return mResticHelper.repoMount(repo, dataDir.absolutePath()); +} + +QString ResticMountManager::generateMountPrivateDir() +{ + // Never empty according to Qt docs. + QStringList paths = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation); + QDir dataDir = QDir(paths[0]); + QString dirName = generateDirName(); + if (!dataDir.exists()) { + if (!QDir().mkpath(dataDir.absolutePath())) { + //*error = i18n("Could not write temporary data into the app private directory"); + qCWarning(KUPRESTICCORE) << "Could not create app temp dir " << dataDir.absolutePath(); + return QString(); + } + } + + if (!dataDir.cd(dirName)) { + qCDebug(KUPRESTICCORE) << "Trying to create mountpoint " + << dataDir.absolutePath() << QDir::separator() << dirName; + if (!dataDir.mkdir(dirName) || !dataDir.cd(dirName)) { + //*error = i18n("Failed to write temporary data into %1", dataDir.absolutePath()); + qCWarning(KUPRESTICCORE) << "Failed to create mountpoint in " << dataDir.absolutePath(); + return QString(); + } + } + + return dataDir.absolutePath(); +} + +ResticMountManager::ResticMountManager() +{ + // Cleanup if possible. + QStringList paths = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation); + QDir dataDir = QDir(paths[0]); + QFileInfoList infoList = dataDir.entryInfoList( + QStringList() << "mount_*", QDir::Dirs | QDir::NoDotAndDotDot); + foreach (QFileInfo info, infoList) { + QDir mountDir(info.absoluteFilePath()); + if (mountDir.entryInfoList(QStringList(), QDir::NoDotAndDotDot).isEmpty()) { + // Try to clean. + if (!QDir().rmdir(info.absoluteFilePath())) + qCWarning(KUPRESTICCORE) << "Cannot cleanup mount path " << info.absoluteFilePath(); + } + } +} + +QString ResticMountManager::generateDirName() +{ + QUuid uuid = QUuid::createUuid(); +#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0) + return QString("mount_%1").arg(uuid.toString(QUuid::Id128)); +#else + return QString("mount_%1").arg(QString::fromLocal8Bit(uuid.toByteArray().toHex())); +#endif +} diff --git a/resticcore/restichelper.h b/resticcore/restichelper.h new file mode 100644 index 0000000..73525f6 --- /dev/null +++ b/resticcore/restichelper.h @@ -0,0 +1,249 @@ +/*************************************************************************** + * Copyright Luca Carlon * + * carlon.luca@gmail.com * + * * + * 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., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#ifndef RESTICHELPER_H +#define RESTICHELPER_H + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "resticforgetswitch.h" + +#define QSL QStringLiteral +#define RESTIC_BIN QSL("restic") + +struct ResticProcess : public KPtyProcess +{ + ResticProcess(const QString &repoPath); + virtual ~ResticProcess() {} +}; + +class MountLock : public QObject +{ + Q_OBJECT +public: + MountLock(const QString& mountPath, QObject* parent = nullptr) : + QObject(parent), mMountPath(mountPath) {} + + virtual bool isLocked() = 0; + virtual bool unlock() = 0; + + QString mountPath() { return mMountPath; } + +signals: + void servingData(); + +protected: + bool umountMountPoint(); + bool removeMountPoint(); + +protected: + QString mMountPath; +}; + +class ResticMountLock : public MountLock +{ + Q_OBJECT +public: + ResticMountLock(const QString &repoPath, const QString& mountPath); + bool isLocked() override; + +public slots: + bool unlock() override; + +private slots: + void onStdoutData(); + +private: + ResticProcess mProcess; + QString mOutput; +}; + +struct ResticProgressState +{ + enum State { + S_SCANNING, + S_TRANSFERING, + S_BUILDING_NEW_INDEX, + S_FIND_DATA_IN_USE, + S_COUNTING, + S_REMOVING + }; + + int mPerc; + qlonglong speedBps; + qint64 processedItems; + qint64 processedBytes; + qint64 totalItems; + qint64 totalBytes; + State state; +}; + +class ResticProgressProcess : public ResticProcess +{ + Q_OBJECT +public: + ResticProgressProcess(const QString &repoPath); + +protected: + virtual bool processStepLine(const QString &line, ResticProgressState &state) = 0; + virtual void emitStepSignal(ResticProgressState &state) = 0; + virtual void emitCompletedSignal() = 0; + virtual void emitFailedSignal() = 0; + +private slots: + void onStdoutData(); + void onFinished(int errorCode, QProcess::ExitStatus exitStatus); +}; + +class ResticForgetProcess : public ResticProgressProcess +{ + Q_OBJECT +public: + ResticForgetProcess(const QString &repoPath, const QList &switches); + +protected: + virtual bool processStepLine(const QString &line, ResticProgressState &state); + virtual void emitStepSignal(ResticProgressState &state) { emit forgetStep(state); } + virtual void emitCompletedSignal() { emit forgetSucceeded(); } + virtual void emitFailedSignal() { emit forgetFailed(); } + +signals: + void forgetStep(ResticProgressState &state); + void forgetSucceeded(); + void forgetFailed(); + +private: + QRegularExpression m_regex; + ResticProgressState::State m_state; +}; + +class ResticBackupProcess : public ResticProgressProcess +{ + Q_OBJECT +public: + ResticBackupProcess(const QString &repoPath, + const QStringList &includedPaths, + const QStringList &excludedPaths, + const QString ®ex); + +protected: + virtual bool processStepLine(const QString &line, ResticProgressState &state) = 0; + + void emitStepSignal(ResticProgressState &state) { emit backupStep(state); } + void emitCompletedSignal() { emit backupCompleted(); } + void emitFailedSignal() { emit backupFailed(); } + +signals: + void backupStep(ResticProgressState state); + void backupCompleted(); + void backupFailed(); + +protected slots: + qint64 processMeasureToBytes(const QString &number, const QString &unit); + +protected: + QRegularExpression mRegexTransfer; +}; + +class ResticBackupProcess08 : public ResticBackupProcess +{ + Q_OBJECT +public: + ResticBackupProcess08(const QString &repoPath, + const QStringList &includedPaths, + const QStringList &excludedPaths) : + ResticBackupProcess(repoPath, includedPaths, excludedPaths, + "\\[.*\\]\\s*([0-9\\.]*)%\\s*([0-9\\.]*)\\s*(GiB|KiB|MiB)""" + "\\s*\\/\\s*([0-9\\.]*)\\s*(GiB|KiB|MiB)\\s*(\\d*)\\s*\\/\\s*" + "(\\d*)[^\\d]*(\\d)*\\s*error") {} + +protected: + virtual bool processStepLine(const QString &line, ResticProgressState &state); +}; + +class ResticBackupProcess09 : public ResticBackupProcess +{ + Q_OBJECT +public: + ResticBackupProcess09(const QString &repoPath, + const QStringList &includedPaths, + const QStringList &excludedPaths) : + ResticBackupProcess(repoPath, includedPaths, excludedPaths, + "\\[\\d+:\\d+\\]\\s+([\\d+\\.]+)%\\s+(\\d+)\\s+files\\s+([\\d\\.]+)" + "\\s+(GiB|MiB|KiB|TiB),\\s+total\\s+\\d+\\s+files\\s+([\\d\\.]+)" + "\\s+(GiB|MiB|KiB|TiB),\\s+(\\d)+\\s+error") {} + +protected: + virtual bool processStepLine(const QString &line, ResticProgressState &state); +}; + +class ResticHelper +{ +public: + ResticHelper(); + + static bool repoExists(const QString &repo); + static bool repoInit(const QString &repo); + static bool repoCheck(const QString &repo, QString &output); + static ResticBackupProcess* repoBackup(const QString &repo, + const QStringList &includedPaths, + const QStringList &excludedPaths); + static ResticMountLock *repoMount(const QString &repo, + const QString &mountPath); + static ResticForgetProcess *repoForget(const QString &repo, + const QList &switches); + + static QStringList resticMountPaths(); + static QList resticMountDirs(); + static QVersionNumber version(); + +private: + static bool isRepoMounted(const QString& repo); +}; + +class ResticMountManager +{ +public: + static ResticMountManager& instance() { + static ResticMountManager _instance; + return _instance; + } + + ResticMountLock *mount(const QString &repo); + + static QString generateMountPrivateDir(); + +private: + ResticMountManager(); + static QString generateDirName(); + +private: + ResticHelper mResticHelper; +}; + +#endif // RESTICHELPER_H diff --git a/settings/backupplan.cpp b/settings/backupplan.cpp index 9f1c1af..543b01a 100644 --- a/settings/backupplan.cpp +++ b/settings/backupplan.cpp @@ -76,6 +76,20 @@ BackupPlan::BackupPlan(int pPlanNumber, KSharedConfigPtr pConfig, QObject *pPare addItemBool(QStringLiteral("Show hidden folders"), mShowHiddenFolders); addItemBool(QStringLiteral("Generate recovery info"), mGenerateRecoveryInfo); addItemBool(QStringLiteral("Check backups"), mCheckBackups); + addItemBool(QStringLiteral("Keep last n"), mKeepLastN); + addItemInt(QStringLiteral("Keep last n value"), mKeepLastNValue); + addItemBool(QStringLiteral("Keep hourly"), mKeepHourly); + addItemInt(QStringLiteral("Keep hourly value"), mKeepHourlyValue); + addItemBool(QStringLiteral("Keep daily"), mKeepDaily); + addItemInt(QStringLiteral("Keep daily value"), mKeepDailyValue); + addItemBool(QStringLiteral("Keep monthly"), mKeepMonthly); + addItemInt(QStringLiteral("Keep monthly value"), mKeepMonthlyValue); + addItemBool(QStringLiteral("Keep yearly"), mKeepYearly); + addItemInt(QStringLiteral("Keep yearly value"), mKeepYearlyValue); + addItemBool(QStringLiteral("Keep within duration"), mKeepWithinDuration); + addItemInt(QStringLiteral("Keep within duration value years"), mKeepWithinDurationYears); + addItemInt(QStringLiteral("Keep within duration value months"), mKeepWithinDurationMonths); + addItemInt(QStringLiteral("Keep within duration value days"), mKeepWithinDurationDays); addItemDateTime(QStringLiteral("Last complete backup"), mLastCompleteBackup); addItemDouble(QStringLiteral("Last backup size"), mLastBackupSize); diff --git a/settings/backupplan.h b/settings/backupplan.h index 43d2f6a..a855f76 100644 --- a/settings/backupplan.h +++ b/settings/backupplan.h @@ -39,7 +39,7 @@ class BackupPlan : public KCoreConfigSkeleton QString mDescription; QStringList mPathsIncluded; QStringList mPathsExcluded; - enum BackupType {BupType = 0, RsyncType}; + enum BackupType {BupType = 0, RsyncType, ResticType}; qint32 mBackupType; enum ScheduleType {MANUAL=0, INTERVAL, USAGE}; @@ -63,6 +63,22 @@ class BackupPlan : public KCoreConfigSkeleton bool mGenerateRecoveryInfo; bool mCheckBackups; + // Restic. + bool mKeepLastN; + int mKeepLastNValue; + bool mKeepHourly; + int mKeepHourlyValue; + bool mKeepDaily; + int mKeepDailyValue; + bool mKeepMonthly; + int mKeepMonthlyValue; + bool mKeepYearly; + int mKeepYearlyValue; + bool mKeepWithinDuration; + int mKeepWithinDurationYears; + int mKeepWithinDurationMonths; + int mKeepWithinDurationDays; + QDateTime mLastCompleteBackup; // Size of the last backup in bytes. double mLastBackupSize; diff --git a/settings/kuputils.cpp b/settings/kuputils.cpp index 04ee380..26b1136 100644 --- a/settings/kuputils.cpp +++ b/settings/kuputils.cpp @@ -20,6 +20,9 @@ #include "kuputils.h" #include +#include +#include +#include void ensureTrailingSlash(QString &pPath) { if(!pPath.endsWith(QDir::separator())) { @@ -36,3 +39,36 @@ void ensureNoTrailingSlash(QString &pPath) { QString lastPartOfPath(const QString &pPath) { return pPath.section(QDir::separator(), -1, -1, QString::SectionSkipEmpty); } + +bool fastDiff(const QString &absPath1, const QString &absPath2) +{ + QFileInfo f1(absPath1); + QFileInfo f2(absPath2); + + if (f1.exists() ^ f2.exists()) + return true; + if (f1.size() != f2.size()) + return true; + if (f1.lastModified() != f2.lastModified()) + return true; + +#if 0 + if (!f1.open(QIODevice::ReadOnly)) + return true; + if (!f2.open(QIODevice::ReadOnly)) + return true; + + QDataStream s1(&f1); + QDataStream s2(&f2); + QByteArray ba1; ba1.resize(512); + QByteArray ba2; ba2.resize(512); + while (!s1.atEnd()) { + s1.readRawData(ba1.data(), 512); + s2.readRawData(ba2.data(), 512); + if (ba1 != ba2) + return true; + } +#endif + + return false; +} diff --git a/settings/kuputils.h b/settings/kuputils.h index 35df462..96ca99b 100644 --- a/settings/kuputils.h +++ b/settings/kuputils.h @@ -28,4 +28,6 @@ void ensureNoTrailingSlash(QString &pPath); QString lastPartOfPath(const QString &pPath); +bool fastDiff(const QString &absPath1, const QString &absPath2); + #endif // KUPUTILS_H