File indexing completed on 2024-04-21 05:46:26

0001 // SPDX-FileCopyrightText: 2020 Simon Persson <simon.persson@mykolab.com>
0002 //
0003 // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0004 
0005 #include "bupjob.h"
0006 #include "kupdaemon_debug.h"
0007 
0008 #include <KLocalizedString>
0009 
0010 #include <QFileInfo>
0011 #include <QThread>
0012 
0013 #include <csignal>
0014 
0015 BupJob::BupJob(BackupPlan &pBackupPlan, const QString &pDestinationPath, const QString &pLogFilePath, KupDaemon *pKupDaemon)
0016    :BackupJob(pBackupPlan, pDestinationPath, pLogFilePath, pKupDaemon)
0017 {
0018     mFsckProcess.setOutputChannelMode(KProcess::SeparateChannels);
0019     mIndexProcess.setOutputChannelMode(KProcess::SeparateChannels);
0020     mSaveProcess.setOutputChannelMode(KProcess::SeparateChannels);
0021     mPar2Process.setOutputChannelMode(KProcess::SeparateChannels);
0022     setCapabilities(KJob::Suspendable);
0023     mHarmlessErrorCount = 0;
0024     mAllErrorsHarmless = false;
0025     mLineBreaksRegExp = QRegularExpression(QStringLiteral("\n|\r"));
0026     mLineBreaksRegExp.optimize();
0027     mNonsenseRegExp = QRegularExpression(QStringLiteral("^(?:Reading index|bloom|midx)"));
0028     mNonsenseRegExp.optimize();
0029     mFileGoneRegExp = QRegularExpression(QStringLiteral("\\[Errno 2\\]"));
0030     mFileGoneRegExp.optimize();
0031     mProgressRegExp = QRegularExpression(QStringLiteral("(\\d+)/(\\d+)k, (\\d+)/(\\d+) files\\) \\S* (?:(\\d+)k/s|)"));
0032     mProgressRegExp.optimize();
0033     mErrorCountRegExp = QRegularExpression(QStringLiteral("^WARNING: (\\d+) errors encountered while saving."));
0034     mErrorCountRegExp.optimize();
0035     mFileInfoRegExp = QRegularExpression(QStringLiteral("^(?: |A|M) \\/"));
0036     mFileInfoRegExp.optimize();
0037 }
0038 
0039 void BupJob::performJob() {
0040     KProcess lPar2Process;
0041     lPar2Process.setOutputChannelMode(KProcess::SeparateChannels);
0042     lPar2Process << QStringLiteral("bup") << QStringLiteral("fsck") << QStringLiteral("--par2-ok");
0043     int lExitCode = lPar2Process.execute();
0044     if(lExitCode < 0) {
0045         jobFinishedError(ErrorWithoutLog, xi18nc("@info notification",
0046                                                  "The <application>bup</application> program is "
0047                                                  "needed but could not be found, maybe it is not installed?"));
0048         return;
0049     }
0050     if(mBackupPlan.mGenerateRecoveryInfo && lExitCode != 0) {
0051         jobFinishedError(ErrorWithoutLog, xi18nc("@info notification",
0052                                                  "The <application>par2</application> program is "
0053                                                  "needed but could not be found, maybe it is not installed?"));
0054         return;
0055     }
0056 
0057     mLogStream << QStringLiteral("Kup is starting bup backup job at ")
0058                << QLocale().toString(QDateTime::currentDateTime())
0059                << Qt::endl << Qt::endl;
0060 
0061     KProcess lInitProcess;
0062     lInitProcess.setOutputChannelMode(KProcess::SeparateChannels);
0063     lInitProcess << QStringLiteral("bup");
0064     lInitProcess << QStringLiteral("-d") << mDestinationPath;
0065     lInitProcess << QStringLiteral("init");
0066     mLogStream << quoteArgs(lInitProcess.program()) << Qt::endl;
0067     if(lInitProcess.execute() != 0) {
0068         mLogStream << QString::fromUtf8(lInitProcess.readAllStandardError()) << Qt::endl;
0069         mLogStream << QStringLiteral("Kup did not successfully complete the bup backup job: "
0070                                      "failed to initialize backup destination.") << Qt::endl;
0071         jobFinishedError(ErrorWithLog, xi18nc("@info notification", "Backup destination could not be initialised. "
0072                                                                     "See log file for more details."));
0073         return;
0074     }
0075 
0076     if(mBackupPlan.mCheckBackups) {
0077         mFsckProcess << QStringLiteral("bup");
0078         mFsckProcess << QStringLiteral("-d") << mDestinationPath;
0079         mFsckProcess << QStringLiteral("fsck") << QStringLiteral("--quick");
0080         mFsckProcess << QStringLiteral("-j") << QString::number(qMin(4, QThread::idealThreadCount()));
0081 
0082         connect(&mFsckProcess, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(slotCheckingDone(int,QProcess::ExitStatus)));
0083         connect(&mFsckProcess, SIGNAL(started()), SLOT(slotCheckingStarted()));
0084         mLogStream << quoteArgs(mFsckProcess.program()) << Qt::endl;
0085         mFsckProcess.start();
0086         mInfoRateLimiter.start();
0087     } else {
0088         startIndexing();
0089     }
0090 }
0091 
0092 void BupJob::slotCheckingStarted() {
0093     makeNice(mFsckProcess.processId());
0094     emit description(this, i18n("Checking backup integrity"));
0095 }
0096 
0097 void BupJob::slotCheckingDone(int pExitCode, QProcess::ExitStatus pExitStatus) {
0098     QString lErrors = QString::fromUtf8(mFsckProcess.readAllStandardError());
0099     if(!lErrors.isEmpty()) {
0100         mLogStream << lErrors << Qt::endl;
0101     }
0102     mLogStream << "Exit code: " << pExitCode << Qt::endl;
0103     if(pExitStatus != QProcess::NormalExit || pExitCode != 0) {
0104         mLogStream << QStringLiteral("Kup did not successfully complete the bup backup job: "
0105                                      "failed integrity check. Your backups could be "
0106                                      "corrupted! See above for details.") << Qt::endl;
0107         if(mBackupPlan.mGenerateRecoveryInfo) {
0108             jobFinishedError(ErrorSuggestRepair, xi18nc("@info notification",
0109                                                         "Failed backup integrity check. Your backups could be corrupted! "
0110                                                         "See log file for more details. Do you want to try repairing the backup files?"));
0111         } else {
0112             jobFinishedError(ErrorWithLog, xi18nc("@info notification",
0113                                                   "Failed backup integrity check. Your backups could be corrupted! "
0114                                                   "See log file for more details."));
0115         }
0116         return;
0117     }
0118     startIndexing();
0119 }
0120 
0121 void BupJob::startIndexing() {
0122     mIndexProcess << QStringLiteral("bup");
0123     mIndexProcess << QStringLiteral("-d") << mDestinationPath;
0124     mIndexProcess << QStringLiteral("index") << QStringLiteral("-u");
0125 
0126     foreach(QString lExclude, mBackupPlan.mPathsExcluded) {
0127         mIndexProcess << QStringLiteral("--exclude");
0128         mIndexProcess << lExclude;
0129     }
0130     QString lExcludesPath = mBackupPlan.absoluteExcludesFilePath();
0131     if(mBackupPlan.mExcludePatterns && QFileInfo::exists(lExcludesPath)) {
0132         mIndexProcess << QStringLiteral("--exclude-rx-from") << lExcludesPath;
0133     }
0134     mIndexProcess << mBackupPlan.mPathsIncluded;
0135 
0136     connect(&mIndexProcess, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(slotIndexingDone(int,QProcess::ExitStatus)));
0137     connect(&mIndexProcess, SIGNAL(started()), SLOT(slotIndexingStarted()));
0138     mLogStream << quoteArgs(mIndexProcess.program()) << Qt::endl;
0139     mIndexProcess.start();
0140 }
0141 
0142 void BupJob::slotIndexingStarted() {
0143     makeNice(mIndexProcess.processId());
0144     emit description(this, i18n("Checking what to copy"));
0145 }
0146 
0147 void BupJob::slotIndexingDone(int pExitCode, QProcess::ExitStatus pExitStatus) {
0148     QString lErrors = QString::fromUtf8(mIndexProcess.readAllStandardError());
0149     if(!lErrors.isEmpty()) {
0150         mLogStream << lErrors << Qt::endl;
0151     }
0152     mLogStream << "Exit code: " << pExitCode << Qt::endl;
0153     if(pExitStatus != QProcess::NormalExit || pExitCode != 0) {
0154         mLogStream << QStringLiteral("Kup did not successfully complete the bup backup job: failed to index everything.") << Qt::endl;
0155         jobFinishedError(ErrorWithLog, xi18nc("@info notification", "Failed to analyze files. "
0156                                                                     "See log file for more details."));
0157         return;
0158     }
0159     mSaveProcess << QStringLiteral("bup");
0160     mSaveProcess << QStringLiteral("-d") << mDestinationPath;
0161     mSaveProcess << QStringLiteral("save");
0162     mSaveProcess << QStringLiteral("-n") << QStringLiteral("kup") << QStringLiteral("-vv");
0163     mSaveProcess << mBackupPlan.mPathsIncluded;
0164     mLogStream << quoteArgs(mSaveProcess.program()) << Qt::endl;
0165 
0166     connect(&mSaveProcess, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(slotSavingDone(int,QProcess::ExitStatus)));
0167     connect(&mSaveProcess, SIGNAL(started()), SLOT(slotSavingStarted()));
0168     connect(&mSaveProcess, &KProcess::readyReadStandardError, this, &BupJob::slotReadBupErrors);
0169 
0170     mSaveProcess.setEnv(QStringLiteral("BUP_FORCE_TTY"), QStringLiteral("2"));
0171     mSaveProcess.start();
0172 }
0173 
0174 void BupJob::slotSavingStarted() {
0175     makeNice(mSaveProcess.processId());
0176     emit description(this, i18n("Saving backup"));
0177 }
0178 
0179 void BupJob::slotSavingDone(int pExitCode, QProcess::ExitStatus pExitStatus) {
0180     slotReadBupErrors();
0181     mLogStream << "Exit code: " << pExitCode << Qt::endl;
0182     if(pExitStatus != QProcess::NormalExit || pExitCode != 0) {
0183         if(mAllErrorsHarmless) {
0184             mLogStream << QStringLiteral("Only harmless errors detected by Kup.") << Qt::endl;
0185         } else {
0186             mLogStream << QStringLiteral("Kup did not successfully complete the bup backup job: "
0187                                          "failed to save everything.") << Qt::endl;
0188             jobFinishedError(ErrorWithLog, xi18nc("@info notification", "Failed to save backup. "
0189                                                                         "See log file for more details."));
0190             return;
0191         }
0192     }
0193     if(mBackupPlan.mGenerateRecoveryInfo) {
0194         mPar2Process << QStringLiteral("bup");
0195         mPar2Process << QStringLiteral("-d") << mDestinationPath;
0196         mPar2Process << QStringLiteral("fsck") << QStringLiteral("-g");
0197         mPar2Process << QStringLiteral("-j") << QString::number(qMin(4, QThread::idealThreadCount()));
0198 
0199         connect(&mPar2Process, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(slotRecoveryInfoDone(int,QProcess::ExitStatus)));
0200         connect(&mPar2Process, SIGNAL(started()), SLOT(slotRecoveryInfoStarted()));
0201         mLogStream << quoteArgs(mPar2Process.program()) << Qt::endl;
0202         mPar2Process.start();
0203     } else {
0204         mLogStream << QStringLiteral("Kup successfully completed the bup backup job at ")
0205                    << QLocale().toString(QDateTime::currentDateTime()) << Qt::endl;
0206         jobFinishedSuccess();
0207     }
0208 }
0209 
0210 void BupJob::slotRecoveryInfoStarted() {
0211     makeNice(mPar2Process.processId());
0212     emit description(this, i18n("Generating recovery information"));
0213 }
0214 
0215 void BupJob::slotRecoveryInfoDone(int pExitCode, QProcess::ExitStatus pExitStatus) {
0216     QString lErrors = QString::fromUtf8(mPar2Process.readAllStandardError());
0217     if(!lErrors.isEmpty()) {
0218         mLogStream << lErrors << Qt::endl;
0219     }
0220     mLogStream << "Exit code: " << pExitCode << Qt::endl;
0221     if(pExitStatus != QProcess::NormalExit || pExitCode != 0) {
0222         mLogStream << QStringLiteral("Kup did not successfully complete the bup backup job: "
0223                                      "failed to generate recovery info.") << Qt::endl;
0224         jobFinishedError(ErrorWithLog, xi18nc("@info notification", "Failed to generate recovery info for the backup. "
0225                                                                     "See log file for more details."));
0226     } else {
0227         mLogStream << QStringLiteral("Kup successfully completed the bup backup job.") << Qt::endl;
0228         jobFinishedSuccess();
0229     }
0230 }
0231 
0232 void BupJob::slotReadBupErrors() {
0233     qulonglong lCopiedKBytes = 0, lTotalKBytes = 0, lCopiedFiles = 0, lTotalFiles = 0;
0234     ulong lSpeedKBps = 0, lPercent = 0;
0235     QString lFileName;
0236     const auto lInput = QString::fromUtf8(mSaveProcess.readAllStandardError());
0237 #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
0238     const auto lLines = lInput.split(mLineBreaksRegExp, QString::SkipEmptyParts);
0239 #else
0240     const auto lLines = lInput.split(mLineBreaksRegExp, Qt::SkipEmptyParts);
0241 #endif
0242     for(const QString &lLine: lLines) {
0243         qCDebug(KUPDAEMON) << lLine;
0244         if(mNonsenseRegExp.match(lLine).hasMatch()) {
0245             continue;
0246         }
0247         if(mFileGoneRegExp.match(lLine).hasMatch()) {
0248             mHarmlessErrorCount++;
0249             mLogStream << lLine << Qt::endl;
0250             continue;
0251         }
0252         const auto lCountMatch = mErrorCountRegExp.match(lLine);
0253         if(lCountMatch.hasMatch()) {
0254             mAllErrorsHarmless = lCountMatch.captured(1).toInt() == mHarmlessErrorCount;
0255             mLogStream << lLine << Qt::endl;
0256             continue;
0257         }
0258         const auto lProgressMatch = mProgressRegExp.match(lLine);
0259         if(lProgressMatch.hasMatch()) {
0260             lCopiedKBytes = lProgressMatch.captured(1).toULongLong();
0261             lTotalKBytes = lProgressMatch.captured(2).toULongLong();
0262             lCopiedFiles = lProgressMatch.captured(3).toULongLong();
0263             lTotalFiles = lProgressMatch.captured(4).toULongLong();
0264             lSpeedKBps = lProgressMatch.captured(5).toULong();
0265             if(lTotalKBytes != 0) {
0266                 lPercent = qMax(100*lCopiedKBytes/lTotalKBytes, static_cast<qulonglong>(1));
0267             }
0268             continue;
0269         }
0270         if(mFileInfoRegExp.match(lLine).hasMatch()) {
0271             lFileName = lLine.mid(2);
0272             continue;
0273         }
0274         if(!lLine.startsWith(QStringLiteral("D /"))) {
0275             mLogStream << lLine << Qt::endl;
0276         }
0277     }
0278     if(mInfoRateLimiter.hasExpired(200)) {
0279         if(lTotalFiles != 0) {
0280             setPercent(lPercent);
0281             setTotalAmount(KJob::Bytes, lTotalKBytes*1024);
0282             setTotalAmount(KJob::Files, lTotalFiles);
0283             setProcessedAmount(KJob::Bytes, lCopiedKBytes*1024);
0284             setProcessedAmount(KJob::Files, lCopiedFiles);
0285             emitSpeed(lSpeedKBps * 1024);
0286         }
0287         if(!lFileName.isEmpty()) {
0288             emit description(this, i18n("Saving backup"),
0289                              qMakePair(i18nc("Label for file currently being copied", "File"), lFileName));
0290         }
0291         mInfoRateLimiter.start();
0292     }
0293 }
0294 
0295 bool BupJob::doSuspend() {
0296     if(mFsckProcess.state() == KProcess::Running) {
0297         return 0 == ::kill(mFsckProcess.processId(), SIGSTOP);
0298     }
0299     if(mIndexProcess.state() == KProcess::Running) {
0300         return 0 == ::kill(mIndexProcess.processId(), SIGSTOP);
0301     }
0302     if(mSaveProcess.state() == KProcess::Running) {
0303         return 0 == ::kill(mSaveProcess.processId(), SIGSTOP);
0304     }
0305     if(mPar2Process.state() == KProcess::Running) {
0306         return 0 == ::kill(mPar2Process.processId(), SIGSTOP);
0307     }
0308     return false;
0309 }
0310 
0311 bool BupJob::doResume() {
0312     if(mFsckProcess.state() == KProcess::Running) {
0313         return 0 == ::kill(mFsckProcess.processId(), SIGCONT);
0314     }
0315     if(mIndexProcess.state() == KProcess::Running) {
0316         return 0 == ::kill(mIndexProcess.processId(), SIGCONT);
0317     }
0318     if(mSaveProcess.state() == KProcess::Running) {
0319         return 0 == ::kill(mSaveProcess.processId(), SIGCONT);
0320     }
0321     if(mPar2Process.state() == KProcess::Running) {
0322         return 0 == ::kill(mPar2Process.processId(), SIGCONT);
0323     }
0324     return false;
0325 }