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"