Warning, file /pim/mailcommon/src/job/backupjob.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).
0001 /* 0002 0003 SPDX-FileCopyrightText: 2009 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com> 0004 0005 SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0006 */ 0007 0008 #include "backupjob.h" 0009 0010 #include "mailcommon_debug.h" 0011 #include <Akonadi/CollectionDeleteJob> 0012 #include <Akonadi/CollectionFetchJob> 0013 #include <Akonadi/CollectionFetchScope> 0014 #include <Akonadi/ItemFetchJob> 0015 #include <Akonadi/ItemFetchScope> 0016 #include <PimCommon/BroadcastStatus> 0017 0018 #include <KMime/Message> 0019 0020 #include <KIO/Global> 0021 #include <KLocalizedString> 0022 #include <KMessageBox> 0023 #include <KTar> 0024 #include <KZip> 0025 0026 #include <QFileInfo> 0027 #include <QTimer> 0028 0029 using namespace MailCommon; 0030 static const mode_t archivePerms = S_IFREG | 0644; 0031 0032 BackupJob::BackupJob(QWidget *parent) 0033 : QObject(parent) 0034 , mArchiveTime(QDateTime::currentDateTime()) 0035 , mRootFolder(0) 0036 , mParentWidget(parent) 0037 , mCurrentFolder(Akonadi::Collection()) 0038 { 0039 } 0040 0041 BackupJob::~BackupJob() 0042 { 0043 mPendingFolders.clear(); 0044 delete mArchive; 0045 mArchive = nullptr; 0046 } 0047 0048 void BackupJob::setRootFolder(const Akonadi::Collection &rootFolder) 0049 { 0050 mRootFolder = rootFolder; 0051 } 0052 0053 void BackupJob::setRealPath(const QString &path) 0054 { 0055 mRealPath = path; 0056 } 0057 0058 void BackupJob::setSaveLocation(const QUrl &savePath) 0059 { 0060 mMailArchivePath = savePath; 0061 } 0062 0063 void BackupJob::setArchiveType(ArchiveType type) 0064 { 0065 mArchiveType = type; 0066 } 0067 0068 void BackupJob::setDeleteFoldersAfterCompletion(bool deleteThem) 0069 { 0070 mDeleteFoldersAfterCompletion = deleteThem; 0071 } 0072 0073 void BackupJob::setRecursive(bool recursive) 0074 { 0075 mRecursive = recursive; 0076 } 0077 0078 bool BackupJob::queueFolders(const Akonadi::Collection &root) 0079 { 0080 mPendingFolders.append(root); 0081 if (mRecursive) { 0082 // FIXME: Get rid of the exec() 0083 // We could do a recursive CollectionFetchJob, but we only fetch the first level 0084 // and then recurse manually. This is needed because a recursive fetch doesn't 0085 // sort the collections the way we want. We need all first level children to be 0086 // in the mPendingFolders list before all second level children, so that the 0087 // directories for the first level are written before the directories in the 0088 // second level, in the archive file. 0089 auto job = new Akonadi::CollectionFetchJob(root, Akonadi::CollectionFetchJob::FirstLevel); 0090 job->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::All); 0091 job->exec(); 0092 if (job->error()) { 0093 qCWarning(MAILCOMMON_LOG) << job->errorString(); 0094 abort(i18n("Unable to retrieve folder list.")); 0095 return false; 0096 } 0097 0098 const Akonadi::Collection::List lstCols = job->collections(); 0099 for (const Akonadi::Collection &collection : lstCols) { 0100 if (!queueFolders(collection)) { 0101 return false; 0102 } 0103 } 0104 } 0105 mAllFolders = mPendingFolders; 0106 return true; 0107 } 0108 0109 bool BackupJob::hasChildren(const Akonadi::Collection &collection) const 0110 { 0111 for (const Akonadi::Collection &curCol : std::as_const(mAllFolders)) { 0112 if (collection == curCol.parentCollection()) { 0113 return true; 0114 } 0115 } 0116 return false; 0117 } 0118 0119 void BackupJob::cancelJob() 0120 { 0121 abort(i18n("The operation was canceled by the user.")); 0122 } 0123 0124 void BackupJob::abort(const QString &errorMessage) 0125 { 0126 // We could be called this twice, since killing the current job below will 0127 // cause the job to fail, and that will call abort() 0128 if (mAborted) { 0129 return; 0130 } 0131 0132 mAborted = true; 0133 if (mCurrentFolder.isValid()) { 0134 mCurrentFolder = Akonadi::Collection(); 0135 } 0136 0137 if (mArchive && mArchive->isOpen()) { 0138 mArchive->close(); 0139 } 0140 0141 if (mCurrentJob) { 0142 mCurrentJob->kill(); 0143 mCurrentJob = nullptr; 0144 } 0145 0146 if (mProgressItem) { 0147 mProgressItem->setComplete(); 0148 mProgressItem = nullptr; 0149 // The progressmanager will delete it 0150 } 0151 QString text = i18n("Failed to archive the folder '%1'.", mRootFolder.name()); 0152 text += QLatin1Char('\n') + errorMessage; 0153 Q_EMIT error(text); 0154 if (mDisplayMessageBox) { 0155 KMessageBox::error(mParentWidget, text, i18n("Archiving failed")); 0156 } 0157 deleteLater(); 0158 // Clean up archive file here? 0159 } 0160 0161 void BackupJob::finish() 0162 { 0163 if (mArchive->isOpen()) { 0164 if (!mArchive->close()) { 0165 abort(i18n("Unable to finalize the archive file.")); 0166 return; 0167 } 0168 } 0169 0170 const QString archivingStr(i18n("Archiving finished")); 0171 PimCommon::BroadcastStatus::instance()->setStatusMsg(archivingStr); 0172 0173 if (mProgressItem) { 0174 mProgressItem->setStatus(archivingStr); 0175 mProgressItem->setComplete(); 0176 mProgressItem = nullptr; 0177 } 0178 0179 const QFileInfo archiveFileInfo(mMailArchivePath.path()); 0180 QString text = i18n( 0181 "Archiving folder '%1' successfully completed. " 0182 "The archive was written to the file '%2'.", 0183 mRealPath.isEmpty() ? mRootFolder.name() : mRealPath, 0184 mMailArchivePath.path()); 0185 text += QLatin1Char('\n') 0186 + i18np("1 message of size %2 was archived.", 0187 "%1 messages with the total size of %2 were archived.", 0188 mArchivedMessages, 0189 KIO::convertSize(mArchivedSize)); 0190 text += QLatin1Char('\n') + i18n("The archive file has a size of %1.", KIO::convertSize(archiveFileInfo.size())); 0191 if (mDisplayMessageBox) { 0192 KMessageBox::information(mParentWidget, text, i18n("Archiving finished")); 0193 } 0194 0195 if (mDeleteFoldersAfterCompletion) { 0196 // Some safety checks first... 0197 if (archiveFileInfo.exists() && (mArchivedSize > 0 || mArchivedMessages == 0)) { 0198 // Sorry for any data loss! 0199 new Akonadi::CollectionDeleteJob(mRootFolder); 0200 } 0201 } 0202 Q_EMIT backupDone(text); 0203 deleteLater(); 0204 } 0205 0206 void BackupJob::archiveNextMessage() 0207 { 0208 if (mAborted) { 0209 return; 0210 } 0211 0212 if (mPendingMessages.isEmpty()) { 0213 qCDebug(MAILCOMMON_LOG) << "===> All messages done in folder " << mCurrentFolder.name(); 0214 archiveNextFolder(); 0215 return; 0216 } 0217 0218 const Akonadi::Item item = mPendingMessages.takeFirst(); 0219 qCDebug(MAILCOMMON_LOG) << "Fetching item with ID" << item.id() << "for folder" << mCurrentFolder.name(); 0220 0221 mCurrentJob = new Akonadi::ItemFetchJob(item); 0222 mCurrentJob->fetchScope().fetchFullPayload(true); 0223 connect(mCurrentJob, &Akonadi::ItemFetchJob::result, this, &BackupJob::itemFetchJobResult); 0224 } 0225 0226 void BackupJob::processMessage(const Akonadi::Item &item) 0227 { 0228 if (mAborted) { 0229 return; 0230 } 0231 0232 const auto message = item.payload<KMime::Message::Ptr>(); 0233 qCDebug(MAILCOMMON_LOG) << "Processing message with subject " << message->subject(false); 0234 const QByteArray messageData = message->encodedContent(); 0235 const qint64 messageSize = messageData.size(); 0236 const QString messageName = QString::number(item.id()); 0237 const QString fileName = pathForCollection(mCurrentFolder) + QLatin1StringView("/cur/") + messageName; 0238 0239 // PORT ME: user and group! 0240 qCDebug(MAILCOMMON_LOG) << "AKONDI PORT: disabled code here!"; 0241 if (!mArchive->writeFile(fileName, messageData, archivePerms, QStringLiteral("user"), QStringLiteral("group"), mArchiveTime, mArchiveTime, mArchiveTime)) { 0242 abort(i18n("Failed to write a message into the archive folder '%1'.", mCurrentFolder.name())); 0243 return; 0244 } 0245 0246 ++mArchivedMessages; 0247 mArchivedSize += messageSize; 0248 0249 // Use a singleshot timer, otherwise the job started in archiveNextMessage() 0250 // will hang 0251 QTimer::singleShot(0, this, &BackupJob::archiveNextMessage); 0252 } 0253 0254 void BackupJob::itemFetchJobResult(KJob *job) 0255 { 0256 if (mAborted) { 0257 return; 0258 } 0259 0260 Q_ASSERT(job == mCurrentJob); 0261 mCurrentJob = nullptr; 0262 0263 if (job->error()) { 0264 Q_ASSERT(mCurrentFolder.isValid()); 0265 qCWarning(MAILCOMMON_LOG) << job->errorString(); 0266 abort(i18n("Downloading a message in folder '%1' failed.", mCurrentFolder.name())); 0267 } else { 0268 auto fetchJob = qobject_cast<Akonadi::ItemFetchJob *>(job); 0269 Q_ASSERT(fetchJob); 0270 Q_ASSERT(fetchJob->items().size() == 1); 0271 processMessage(fetchJob->items().constFirst()); 0272 } 0273 } 0274 0275 bool BackupJob::writeDirHelper(const QString &directoryPath) 0276 { 0277 // PORT ME: Correct user/group 0278 qCDebug(MAILCOMMON_LOG) << "AKONDI PORT: Disabled code here!"; 0279 return mArchive->writeDir(directoryPath, QStringLiteral("user"), QStringLiteral("group"), 040755, mArchiveTime, mArchiveTime, mArchiveTime); 0280 } 0281 0282 QString BackupJob::collectionName(const Akonadi::Collection &collection) const 0283 { 0284 for (const Akonadi::Collection &curCol : std::as_const(mAllFolders)) { 0285 if (curCol == collection) { 0286 return curCol.name(); 0287 } 0288 } 0289 Q_ASSERT(false); 0290 return {}; 0291 } 0292 0293 QString BackupJob::pathForCollection(const Akonadi::Collection &collection) const 0294 { 0295 QString fullPath = collectionName(collection); 0296 Akonadi::Collection curCol = collection.parentCollection(); 0297 if (collection != mRootFolder) { 0298 Q_ASSERT(curCol.isValid()); 0299 while (curCol != mRootFolder) { 0300 fullPath.prepend(QLatin1Char('.') + collectionName(curCol) + QLatin1StringView(".directory/")); 0301 curCol = curCol.parentCollection(); 0302 } 0303 Q_ASSERT(curCol == mRootFolder); 0304 fullPath.prepend(QLatin1Char('.') + collectionName(curCol) + QLatin1StringView(".directory/")); 0305 } 0306 return fullPath; 0307 } 0308 0309 QString BackupJob::subdirPathForCollection(const Akonadi::Collection &collection) const 0310 { 0311 QString path = pathForCollection(collection); 0312 const int parentDirEndIndex = path.lastIndexOf(collection.name()); 0313 Q_ASSERT(parentDirEndIndex != -1); 0314 path.truncate(parentDirEndIndex); 0315 path.append(QLatin1Char('.') + collection.name() + QLatin1StringView(".directory")); 0316 return path; 0317 } 0318 0319 void BackupJob::archiveNextFolder() 0320 { 0321 if (mAborted) { 0322 return; 0323 } 0324 0325 if (mPendingFolders.isEmpty()) { 0326 finish(); 0327 return; 0328 } 0329 0330 mCurrentFolder = mPendingFolders.takeAt(0); 0331 qCDebug(MAILCOMMON_LOG) << "===> Archiving next folder: " << mCurrentFolder.name(); 0332 const QString archivingStr(i18n("Archiving folder %1", mCurrentFolder.name())); 0333 if (mProgressItem) { 0334 mProgressItem->setStatus(archivingStr); 0335 } 0336 PimCommon::BroadcastStatus::instance()->setStatusMsg(archivingStr); 0337 0338 const QString folderName = mCurrentFolder.name(); 0339 bool success = true; 0340 if (hasChildren(mCurrentFolder)) { 0341 if (!writeDirHelper(subdirPathForCollection(mCurrentFolder))) { 0342 success = false; 0343 } 0344 } 0345 if (success) { 0346 if (!writeDirHelper(pathForCollection(mCurrentFolder))) { 0347 success = false; 0348 } else if (!writeDirHelper(pathForCollection(mCurrentFolder) + QLatin1StringView("/cur"))) { 0349 success = false; 0350 } else if (!writeDirHelper(pathForCollection(mCurrentFolder) + QLatin1StringView("/new"))) { 0351 success = false; 0352 } else if (!writeDirHelper(pathForCollection(mCurrentFolder) + QLatin1StringView("/tmp"))) { 0353 success = false; 0354 } 0355 } 0356 if (!success) { 0357 abort(i18n("Unable to create folder structure for folder '%1' within archive file.", mCurrentFolder.name())); 0358 return; 0359 } 0360 auto job = new Akonadi::ItemFetchJob(mCurrentFolder); 0361 job->setProperty("folderName", folderName); 0362 connect(job, &Akonadi::ItemFetchJob::result, this, &BackupJob::onArchiveNextFolderDone); 0363 } 0364 0365 void BackupJob::onArchiveNextFolderDone(KJob *job) 0366 { 0367 if (job->error()) { 0368 qCWarning(MAILCOMMON_LOG) << job->errorString(); 0369 abort(i18n("Unable to get message list for folder %1.", job->property("folderName").toString())); 0370 return; 0371 } 0372 0373 auto fetchJob = qobject_cast<Akonadi::ItemFetchJob *>(job); 0374 mPendingMessages += fetchJob->items(); 0375 archiveNextMessage(); 0376 } 0377 0378 void BackupJob::start() 0379 { 0380 Q_ASSERT(!mMailArchivePath.isEmpty()); 0381 Q_ASSERT(mRootFolder.isValid()); 0382 0383 if (!queueFolders(mRootFolder)) { 0384 return; 0385 } 0386 0387 switch (mArchiveType) { 0388 case Zip: { 0389 KZip *zip = new KZip(mMailArchivePath.path()); 0390 zip->setCompression(KZip::DeflateCompression); 0391 mArchive = zip; 0392 break; 0393 } 0394 case Tar: 0395 mArchive = new KTar(mMailArchivePath.path(), QStringLiteral("application/x-tar")); 0396 break; 0397 case TarGz: 0398 mArchive = new KTar(mMailArchivePath.path(), QStringLiteral("application/x-gzip")); 0399 break; 0400 case TarBz2: 0401 mArchive = new KTar(mMailArchivePath.path(), QStringLiteral("application/x-bzip2")); 0402 break; 0403 } 0404 0405 qCDebug(MAILCOMMON_LOG) << "Starting backup."; 0406 if (!mArchive->open(QIODevice::WriteOnly)) { 0407 abort(i18n("Unable to open archive for writing.")); 0408 return; 0409 } 0410 0411 mProgressItem = KPIM::ProgressManager::createProgressItem(QStringLiteral("BackupJob"), i18n("Archiving"), QString(), true); 0412 mProgressItem->setUsesBusyIndicator(true); 0413 connect(mProgressItem.data(), &KPIM::ProgressItem::progressItemCanceled, this, &BackupJob::cancelJob); 0414 0415 archiveNextFolder(); 0416 } 0417 0418 void BackupJob::setDisplayMessageBox(bool display) 0419 { 0420 mDisplayMessageBox = display; 0421 } 0422 0423 #include "moc_backupjob.cpp"