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