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 }