File indexing completed on 2024-04-21 16:30:25

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 "rsyncjob.h"
0006 #include "kuputils.h"
0007 
0008 #include <csignal>
0009 
0010 #include <QDir>
0011 #include <QRegularExpression>
0012 #include <QTextStream>
0013 
0014 #include <KLocalizedString>
0015 
0016 
0017 RsyncJob::RsyncJob(BackupPlan &pBackupPlan, const QString &pDestinationPath,
0018                    const QString &pLogFilePath, KupDaemon *pKupDaemon)
0019    :BackupJob(pBackupPlan, pDestinationPath, pLogFilePath, pKupDaemon)
0020 {
0021     mRsyncProcess.setOutputChannelMode(KProcess::SeparateChannels);
0022     setCapabilities(KJob::Suspendable | KJob::Killable);
0023 }
0024 
0025 void RsyncJob::performJob() {
0026     KProcess lVersionProcess;
0027     lVersionProcess.setOutputChannelMode(KProcess::SeparateChannels);
0028     lVersionProcess << QStringLiteral("rsync") << QStringLiteral("--version");
0029     if(lVersionProcess.execute() < 0) {
0030         jobFinishedError(ErrorWithoutLog,
0031                          xi18nc("@info notification",
0032                                 "The <application>rsync</application> program is needed but "
0033                                 "could not be found, maybe it is not installed?"));
0034         return;
0035     }
0036 
0037     // Remove this and the performMigration method when it is likely that all users of pre 0.8 kup have now started using post 0.8.
0038     if(mBackupPlan.mBackupVersion < 1 && mBackupPlan.mLastCompleteBackup.isValid() && mBackupPlan.mPathsIncluded.length() == 1) {
0039         mLogStream << QStringLiteral("Migrating saved files to new location, after update to version 0.8 of Kup.") << Qt::endl;
0040         if(!performMigration()) {
0041             mLogStream << QStringLiteral("Migration failed. Continuing backup save regardless, may result in files stored twice.") << Qt::endl;
0042         }
0043     }
0044     mBackupPlan.mBackupVersion = 1;
0045     mBackupPlan.save();
0046 
0047     mLogStream << QStringLiteral("Kup is starting rsync backup job at ")
0048                << QLocale().toString(QDateTime::currentDateTime())
0049                << Qt::endl;
0050 
0051     emit description(this, i18n("Checking what to copy"));
0052     mRsyncProcess << QStringLiteral("rsync") << QStringLiteral("-avX")
0053                   << QStringLiteral("--delete-excluded") << QStringLiteral("--delete-before")
0054                   << QStringLiteral("--info=progress2");
0055 
0056     QStringList lIncludeNames;
0057     foreach(const QString &lInclude, mBackupPlan.mPathsIncluded) {
0058         lIncludeNames << lastPartOfPath(lInclude);
0059     }
0060     if(lIncludeNames.removeDuplicates() > 0) {
0061         // There would be a naming conflict in the destination folder, instead use full paths.
0062         mRsyncProcess << QStringLiteral("-R");
0063         foreach(const QString &lExclude, mBackupPlan.mPathsExcluded) {
0064             mRsyncProcess << QStringLiteral("--exclude") << lExclude;
0065         }
0066     } else {
0067         // when NOT using -R, need to then strip parent paths from excludes, everything above the
0068         // include. Leave the leading slash!
0069         foreach(QString lExclude, mBackupPlan.mPathsExcluded) {
0070             for(int i = 0; i < mBackupPlan.mPathsIncluded.length(); ++i) {
0071                 const QString &lInclude = mBackupPlan.mPathsIncluded.at(i);
0072                 QString lIncludeWithSlash = lInclude;
0073                 ensureTrailingSlash(lIncludeWithSlash);
0074                 if(lExclude.startsWith(lIncludeWithSlash)) {
0075                     lExclude.remove(0, lInclude.length() - lIncludeNames.at(i).length() - 1);
0076                     break;
0077                 }
0078             }
0079             mRsyncProcess << QStringLiteral("--exclude") << lExclude;
0080         }
0081     }
0082     QString lExcludesPath = mBackupPlan.absoluteExcludesFilePath();
0083     if(mBackupPlan.mExcludePatterns && QFileInfo::exists(lExcludesPath)) {
0084         mRsyncProcess << QStringLiteral("--exclude-from") << lExcludesPath;
0085     }
0086     mRsyncProcess << mBackupPlan.mPathsIncluded;
0087     mRsyncProcess << mDestinationPath;
0088 
0089     connect(&mRsyncProcess, SIGNAL(started()), SLOT(slotRsyncStarted()));
0090     connect(&mRsyncProcess, &KProcess::readyReadStandardOutput, this, &RsyncJob::slotReadRsyncOutput);
0091     connect(&mRsyncProcess, SIGNAL(finished(int,QProcess::ExitStatus)),
0092             SLOT(slotRsyncFinished(int,QProcess::ExitStatus)));
0093     mLogStream << quoteArgs(mRsyncProcess.program()) << Qt::endl;
0094     mRsyncProcess.start();
0095     mInfoRateLimiter.start();
0096 }
0097 
0098 void RsyncJob::slotRsyncStarted() {
0099     makeNice(mRsyncProcess.processId());
0100 }
0101 
0102 void RsyncJob::slotRsyncFinished(int pExitCode, QProcess::ExitStatus pExitStatus) {
0103     QString lErrors = QString::fromUtf8(mRsyncProcess.readAllStandardError());
0104     if(!lErrors.isEmpty()) {
0105         mLogStream << lErrors << Qt::endl;
0106     }
0107     mLogStream << "Exit code: " << pExitCode << Qt::endl;
0108     // exit code 24 means source files disappeared during copying. No reason to worry about that.
0109     if(pExitStatus != QProcess::NormalExit || (pExitCode != 0 && pExitCode != 24)) {
0110         mLogStream << QStringLiteral("Kup did not successfully complete the rsync backup job.") << Qt::endl;
0111         jobFinishedError(ErrorWithLog, xi18nc("@info notification", "Failed to save backup. "
0112                                                                     "See log file for more details."));
0113     } else {
0114         mLogStream << QStringLiteral("Kup successfully completed the rsync backup job at ")
0115                    << QLocale().toString(QDateTime::currentDateTime()) << Qt::endl;
0116         jobFinishedSuccess();
0117     }
0118 }
0119 
0120 void RsyncJob::slotReadRsyncOutput() {
0121     bool lValidInfo = false;
0122     bool lValidFileName = false;
0123     QString lFileName;
0124     ulong lPercent{};
0125     qulonglong lTransfered{};
0126     double lSpeed{};
0127     QChar lUnit;
0128     QRegularExpression lProgressInfoExp(QStringLiteral("^\\s+([\\d,\\.]+)\\s+(\\d+)%\\s+(\\d*[,\\.]\\d+)(\\S)"));
0129     // very ugly and rough indication that this is a file path... what else to do..
0130     QRegularExpression lNotFileNameExp(QStringLiteral("^(building file list|done$|deleting \\S+|.+/$|$)"));
0131     QString lLine;
0132 
0133     QTextStream lStream(mRsyncProcess.readAllStandardOutput());
0134     while(lStream.readLineInto(&lLine, 500)) {
0135         QRegularExpressionMatch lMatch = lProgressInfoExp.match(lLine);
0136         if(lMatch.hasMatch()) {
0137             lValidInfo = true;
0138             lTransfered = lMatch.captured(1).remove(',').remove('.').toULongLong();
0139             lPercent = qMax(lMatch.captured(2).toULong(), 1UL);
0140             lSpeed = QLocale().toDouble(lMatch.captured(3));
0141             lUnit = lMatch.captured(4).at(0);
0142         } else {
0143             lMatch = lNotFileNameExp.match(lLine);
0144             if(!lMatch.hasMatch()) {
0145                 lValidFileName = true;
0146                 lFileName = lLine;
0147             }
0148         }
0149     }
0150     if(mInfoRateLimiter.hasExpired(200)) {
0151         if(lValidInfo) {
0152             setPercent(lPercent);
0153             if(lUnit == 'k') {
0154                 lSpeed *= 1e3;
0155             } else if(lUnit == 'M') {
0156                 lSpeed *= 1e6;
0157             } else if(lUnit == 'G') {
0158                 lSpeed *= 1e9;
0159             }
0160             emitSpeed(static_cast<ulong>(lSpeed));
0161             if(lPercent > 5) { // the rounding to integer percent gives big error with small percentages
0162                 setProcessedAmount(KJob::Bytes, lTransfered);
0163                 setTotalAmount(KJob::Bytes, lTransfered*100/lPercent);
0164             }
0165         }
0166         if(lValidFileName) {
0167             emit description(this, i18n("Saving backup"),
0168                              qMakePair(i18nc("Label for file currently being copied", "File"), lFileName));
0169         }
0170         mInfoRateLimiter.start();
0171     }
0172 }
0173 
0174 bool RsyncJob::doKill() {
0175     setError(KilledJobError);
0176     if(0 == ::kill(mRsyncProcess.processId(), SIGINT)) {
0177         return mRsyncProcess.waitForFinished();
0178     }
0179     return false;
0180 }
0181 
0182 bool RsyncJob::doSuspend() {
0183     return 0 == ::kill(mRsyncProcess.processId(), SIGSTOP);
0184 }
0185 
0186 bool RsyncJob::doResume() {
0187     return 0 == ::kill(mRsyncProcess.processId(), SIGCONT);
0188 }
0189 
0190 // This migration moves files from being stored directly in destination folder, to
0191 // being stored in a subfolder of the destination. The subfolder is named same as the
0192 // source folder. This migration will only be done if there is exactly one source folder.
0193 bool RsyncJob::performMigration() {
0194     QString lSourceDirName = lastPartOfPath(mBackupPlan.mPathsIncluded.first()); //only one included
0195     QDir lDestDir = QDir(mDestinationPath);
0196     mLogStream << QStringLiteral("Creating directory named ") << lSourceDirName << " inside of " << mDestinationPath << Qt::endl;
0197     if(!lDestDir.mkdir(lSourceDirName)) {
0198         mLogStream << QStringLiteral("Failed to create directory, aborting migration.") << Qt::endl;
0199         return false;
0200     }
0201     foreach(const QString &lContent, lDestDir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot)) {
0202         if(lContent != lSourceDirName) {
0203             QString lDest = lSourceDirName + QLatin1Char('/') + lContent;
0204             mLogStream << QStringLiteral("Renaming ") << lContent << " to " << lDest << Qt::endl;
0205             if(!lDestDir.rename(lContent, lDest)) {
0206                 mLogStream << QStringLiteral("Failed to rename, aborting migration.") << Qt::endl;
0207                 return false;
0208             }
0209         }
0210     }
0211     mLogStream << QStringLiteral("File migration completed.") << Qt::endl;
0212     return true;
0213 }
0214