Mailcommon

backupjob.cpp
1/*
2
3 SPDX-FileCopyrightText: 2009 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
4
5 SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
6*/
7
8#include "backupjob.h"
9
10#include "mailcommon_debug.h"
11#include <Akonadi/CollectionDeleteJob>
12#include <Akonadi/CollectionFetchJob>
13#include <Akonadi/CollectionFetchScope>
14#include <Akonadi/ItemFetchJob>
15#include <Akonadi/ItemFetchScope>
16#include <PimCommon/BroadcastStatus>
17
18#include <KMime/Message>
19
20#include <KFormat>
21#include <KLocalizedString>
22#include <KMessageBox>
23#include <KTar>
24#include <KZip>
25
26#include <QFileInfo>
27#include <QTimer>
28
29using namespace MailCommon;
30static const mode_t archivePerms = S_IFREG | 0644;
31
32BackupJob::BackupJob(QWidget *parent)
33 : QObject(parent)
34 , mArchiveTime(QDateTime::currentDateTime())
35 , mRootFolder(0)
36 , mParentWidget(parent)
37 , mCurrentFolder(Akonadi::Collection())
38{
39}
40
41BackupJob::~BackupJob()
42{
43 mPendingFolders.clear();
44 delete mArchive;
45 mArchive = nullptr;
46}
47
48void BackupJob::setRootFolder(const Akonadi::Collection &rootFolder)
49{
50 mRootFolder = rootFolder;
51}
52
53void BackupJob::setRealPath(const QString &path)
54{
55 mRealPath = path;
56}
57
58void BackupJob::setSaveLocation(const QUrl &savePath)
59{
60 mMailArchivePath = savePath;
61}
62
63void BackupJob::setArchiveType(ArchiveType type)
64{
65 mArchiveType = type;
66}
67
68void BackupJob::setDeleteFoldersAfterCompletion(bool deleteThem)
69{
70 mDeleteFoldersAfterCompletion = deleteThem;
71}
72
73void BackupJob::setRecursive(bool recursive)
74{
75 mRecursive = recursive;
76}
77
78bool BackupJob::queueFolders(const Akonadi::Collection &root)
79{
80 mPendingFolders.append(root);
81 if (mRecursive) {
82 // FIXME: Get rid of the exec()
83 // We could do a recursive CollectionFetchJob, but we only fetch the first level
84 // and then recurse manually. This is needed because a recursive fetch doesn't
85 // sort the collections the way we want. We need all first level children to be
86 // in the mPendingFolders list before all second level children, so that the
87 // directories for the first level are written before the directories in the
88 // second level, in the archive file.
90 job->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::All);
91 job->exec();
92 if (job->error()) {
93 qCWarning(MAILCOMMON_LOG) << job->errorString();
94 abort(i18n("Unable to retrieve folder list."));
95 return false;
96 }
97
98 const Akonadi::Collection::List lstCols = job->collections();
99 for (const Akonadi::Collection &collection : lstCols) {
100 if (!queueFolders(collection)) {
101 return false;
102 }
103 }
104 }
105 mAllFolders = mPendingFolders;
106 return true;
107}
108
109bool BackupJob::hasChildren(const Akonadi::Collection &collection) const
110{
111 for (const Akonadi::Collection &curCol : std::as_const(mAllFolders)) {
112 if (collection == curCol.parentCollection()) {
113 return true;
114 }
115 }
116 return false;
117}
118
119void BackupJob::cancelJob()
120{
121 abort(i18n("The operation was canceled by the user."));
122}
123
124void BackupJob::abort(const QString &errorMessage)
125{
126 // We could be called this twice, since killing the current job below will
127 // cause the job to fail, and that will call abort()
128 if (mAborted) {
129 return;
130 }
131
132 mAborted = true;
133 if (mCurrentFolder.isValid()) {
134 mCurrentFolder = Akonadi::Collection();
135 }
136
137 if (mArchive && mArchive->isOpen()) {
138 mArchive->close();
139 }
140
141 if (mCurrentJob) {
142 mCurrentJob->kill();
143 mCurrentJob = nullptr;
144 }
145
146 if (mProgressItem) {
147 mProgressItem->setComplete();
148 mProgressItem = nullptr;
149 // The progressmanager will delete it
150 }
151 QString text = i18n("Failed to archive the folder '%1'.", mRootFolder.name());
152 text += QLatin1Char('\n') + errorMessage;
153 Q_EMIT error(text);
154 if (mDisplayMessageBox) {
155 KMessageBox::error(mParentWidget, text, i18nc("@title:window", "Archiving failed"));
156 }
157 deleteLater();
158 // Clean up archive file here?
159}
160
161void BackupJob::finish()
162{
163 if (mArchive->isOpen()) {
164 if (!mArchive->close()) {
165 abort(i18n("Unable to finalize the archive file."));
166 return;
167 }
168 }
169
170 const QString archivingStr(i18n("Archiving finished"));
171 PimCommon::BroadcastStatus::instance()->setStatusMsg(archivingStr);
172
173 if (mProgressItem) {
174 mProgressItem->setStatus(archivingStr);
175 mProgressItem->setComplete();
176 mProgressItem = nullptr;
177 }
178
179 const QFileInfo archiveFileInfo(mMailArchivePath.path());
180 QString text = i18n(
181 "Archiving folder '%1' successfully completed. "
182 "The archive was written to the file '%2'.",
183 mRealPath.isEmpty() ? mRootFolder.name() : mRealPath,
184 mMailArchivePath.path());
185 KFormat format;
186 text += QLatin1Char('\n')
187 + i18np("1 message of size %2 was archived.",
188 "%1 messages with the total size of %2 were archived.",
189 mArchivedMessages,
190 format.formatByteSize(mArchivedSize));
191 text += QLatin1Char('\n') + i18n("The archive file has a size of %1.", format.formatByteSize(archiveFileInfo.size()));
192 if (mDisplayMessageBox) {
193 KMessageBox::information(mParentWidget, text, i18nc("@title:window", "Archiving finished"));
194 }
195
196 if (mDeleteFoldersAfterCompletion) {
197 // Some safety checks first...
198 if (archiveFileInfo.exists() && (mArchivedSize > 0 || mArchivedMessages == 0)) {
199 // Sorry for any data loss!
200 new Akonadi::CollectionDeleteJob(mRootFolder);
201 }
202 }
203 Q_EMIT backupDone(text);
204 deleteLater();
205}
206
207void BackupJob::archiveNextMessage()
208{
209 if (mAborted) {
210 return;
211 }
212
213 if (mPendingMessages.isEmpty()) {
214 qCDebug(MAILCOMMON_LOG) << "===> All messages done in folder " << mCurrentFolder.name();
215 archiveNextFolder();
216 return;
217 }
218
219 const Akonadi::Item item = mPendingMessages.takeFirst();
220 qCDebug(MAILCOMMON_LOG) << "Fetching item with ID" << item.id() << "for folder" << mCurrentFolder.name();
221
222 mCurrentJob = new Akonadi::ItemFetchJob(item);
223 mCurrentJob->fetchScope().fetchFullPayload(true);
224 connect(mCurrentJob, &Akonadi::ItemFetchJob::result, this, &BackupJob::itemFetchJobResult);
225}
226
227void BackupJob::processMessage(const Akonadi::Item &item)
228{
229 if (mAborted) {
230 return;
231 }
232
233 const auto message = item.payload<KMime::Message::Ptr>();
234 qCDebug(MAILCOMMON_LOG) << "Processing message with subject " << message->subject(false);
235 const QByteArray messageData = message->encodedContent();
236 const qint64 messageSize = messageData.size();
237 const QString messageName = QString::number(item.id());
238 const QString fileName = pathForCollection(mCurrentFolder) + QLatin1StringView("/cur/") + messageName;
239
240 // PORT ME: user and group!
241 qCDebug(MAILCOMMON_LOG) << "AKONDI PORT: disabled code here!";
242 if (!mArchive->writeFile(fileName, messageData, archivePerms, QStringLiteral("user"), QStringLiteral("group"), mArchiveTime, mArchiveTime, mArchiveTime)) {
243 abort(i18n("Failed to write a message into the archive folder '%1'.", mCurrentFolder.name()));
244 return;
245 }
246
247 ++mArchivedMessages;
248 mArchivedSize += messageSize;
249
250 // Use a singleshot timer, otherwise the job started in archiveNextMessage()
251 // will hang
252 QTimer::singleShot(0, this, &BackupJob::archiveNextMessage);
253}
254
255void BackupJob::itemFetchJobResult(KJob *job)
256{
257 if (mAborted) {
258 return;
259 }
260
261 Q_ASSERT(job == mCurrentJob);
262 mCurrentJob = nullptr;
263
264 if (job->error()) {
265 Q_ASSERT(mCurrentFolder.isValid());
266 qCWarning(MAILCOMMON_LOG) << job->errorString();
267 abort(i18n("Downloading a message in folder '%1' failed.", mCurrentFolder.name()));
268 } else {
269 auto fetchJob = qobject_cast<Akonadi::ItemFetchJob *>(job);
270 Q_ASSERT(fetchJob);
271 Q_ASSERT(fetchJob->items().size() == 1);
272 processMessage(fetchJob->items().constFirst());
273 }
274}
275
276bool BackupJob::writeDirHelper(const QString &directoryPath)
277{
278 // PORT ME: Correct user/group
279 qCDebug(MAILCOMMON_LOG) << "AKONDI PORT: Disabled code here!";
280 return mArchive->writeDir(directoryPath, QStringLiteral("user"), QStringLiteral("group"), 040755, mArchiveTime, mArchiveTime, mArchiveTime);
281}
282
283QString BackupJob::collectionName(const Akonadi::Collection &collection) const
284{
285 for (const Akonadi::Collection &curCol : std::as_const(mAllFolders)) {
286 if (curCol == collection) {
287 return curCol.name();
288 }
289 }
290 Q_ASSERT(false);
291 return {};
292}
293
294QString BackupJob::pathForCollection(const Akonadi::Collection &collection) const
295{
296 QString fullPath = collectionName(collection);
297 Akonadi::Collection curCol = collection.parentCollection();
298 if (collection != mRootFolder) {
299 Q_ASSERT(curCol.isValid());
300 while (curCol != mRootFolder) {
301 fullPath.prepend(QLatin1Char('.') + collectionName(curCol) + QLatin1StringView(".directory/"));
302 curCol = curCol.parentCollection();
303 }
304 Q_ASSERT(curCol == mRootFolder);
305 fullPath.prepend(QLatin1Char('.') + collectionName(curCol) + QLatin1StringView(".directory/"));
306 }
307 return fullPath;
308}
309
310QString BackupJob::subdirPathForCollection(const Akonadi::Collection &collection) const
311{
312 QString path = pathForCollection(collection);
313 const int parentDirEndIndex = path.lastIndexOf(collection.name());
314 Q_ASSERT(parentDirEndIndex != -1);
315 path.truncate(parentDirEndIndex);
316 path.append(QLatin1Char('.') + collection.name() + QLatin1StringView(".directory"));
317 return path;
318}
319
320void BackupJob::archiveNextFolder()
321{
322 if (mAborted) {
323 return;
324 }
325
326 if (mPendingFolders.isEmpty()) {
327 finish();
328 return;
329 }
330
331 mCurrentFolder = mPendingFolders.takeAt(0);
332 qCDebug(MAILCOMMON_LOG) << "===> Archiving next folder: " << mCurrentFolder.name();
333 const QString archivingStr(i18n("Archiving folder %1", mCurrentFolder.name()));
334 if (mProgressItem) {
335 mProgressItem->setStatus(archivingStr);
336 }
337 PimCommon::BroadcastStatus::instance()->setStatusMsg(archivingStr);
338
339 const QString folderName = mCurrentFolder.name();
340 bool success = true;
341 if (hasChildren(mCurrentFolder)) {
342 if (!writeDirHelper(subdirPathForCollection(mCurrentFolder))) {
343 success = false;
344 }
345 }
346 if (success) {
347 if (!writeDirHelper(pathForCollection(mCurrentFolder))) {
348 success = false;
349 } else if (!writeDirHelper(pathForCollection(mCurrentFolder) + QLatin1StringView("/cur"))) {
350 success = false;
351 } else if (!writeDirHelper(pathForCollection(mCurrentFolder) + QLatin1StringView("/new"))) {
352 success = false;
353 } else if (!writeDirHelper(pathForCollection(mCurrentFolder) + QLatin1StringView("/tmp"))) {
354 success = false;
355 }
356 }
357 if (!success) {
358 abort(i18n("Unable to create folder structure for folder '%1' within archive file.", mCurrentFolder.name()));
359 return;
360 }
361 auto job = new Akonadi::ItemFetchJob(mCurrentFolder);
362 job->setProperty("folderName", folderName);
363 connect(job, &Akonadi::ItemFetchJob::result, this, &BackupJob::onArchiveNextFolderDone);
364}
365
366void BackupJob::onArchiveNextFolderDone(KJob *job)
367{
368 if (job->error()) {
369 qCWarning(MAILCOMMON_LOG) << job->errorString();
370 abort(i18n("Unable to get message list for folder %1.", job->property("folderName").toString()));
371 return;
372 }
373
374 auto fetchJob = qobject_cast<Akonadi::ItemFetchJob *>(job);
375 mPendingMessages += fetchJob->items();
376 archiveNextMessage();
377}
378
379void BackupJob::start()
380{
381 Q_ASSERT(!mMailArchivePath.isEmpty());
382 Q_ASSERT(mRootFolder.isValid());
383
384 if (!queueFolders(mRootFolder)) {
385 return;
386 }
387
388 switch (mArchiveType) {
389 case Zip: {
390 KZip *zip = new KZip(mMailArchivePath.path());
392 mArchive = zip;
393 break;
394 }
395 case Tar:
396 mArchive = new KTar(mMailArchivePath.path(), QStringLiteral("application/x-tar"));
397 break;
398 case TarGz:
399 mArchive = new KTar(mMailArchivePath.path(), QStringLiteral("application/x-gzip"));
400 break;
401 case TarBz2:
402 mArchive = new KTar(mMailArchivePath.path(), QStringLiteral("application/x-bzip2"));
403 break;
404 }
405
406 qCDebug(MAILCOMMON_LOG) << "Starting backup.";
407 if (!mArchive->open(QIODevice::WriteOnly)) {
408 abort(i18n("Unable to open archive for writing."));
409 return;
410 }
411
412 mProgressItem = KPIM::ProgressManager::createProgressItem(QStringLiteral("BackupJob"), i18n("Archiving"), QString(), true);
413 mProgressItem->setUsesBusyIndicator(true);
414 connect(mProgressItem.data(), &KPIM::ProgressItem::progressItemCanceled, this, &BackupJob::cancelJob);
415
416 archiveNextFolder();
417}
418
419void BackupJob::setDisplayMessageBox(bool display)
420{
421 mDisplayMessageBox = display;
422}
423
424#include "moc_backupjob.cpp"
bool isValid() const
QString name() const
Collection & parentCollection()
ItemFetchScope & fetchScope()
void fetchFullPayload(bool fetch=true)
Id id() const
T payload() const
virtual bool close()
virtual bool open(QIODevice::OpenMode mode)
bool writeFile(const QString &name, QByteArrayView data, mode_t perm=0100644, const QString &user=QString(), const QString &group=QString(), const QDateTime &atime=QDateTime(), const QDateTime &mtime=QDateTime(), const QDateTime &ctime=QDateTime())
bool writeDir(const QString &name, const QString &user=QString(), const QString &group=QString(), mode_t perm=040755, const QDateTime &atime=QDateTime(), const QDateTime &mtime=QDateTime(), const QDateTime &ctime=QDateTime())
bool isOpen() const
QString formatByteSize(double size, int precision=1, KFormat::BinaryUnitDialect dialect=KFormat::DefaultBinaryDialect, KFormat::BinarySizeUnits units=KFormat::DefaultBinaryUnits) const
virtual QString errorString() const
int error() const
void result(KJob *job)
bool kill(KJob::KillVerbosity verbosity=KJob::Quietly)
void progressItemCanceled(KPIM::ProgressItem *)
static ProgressItem * createProgressItem(const QString &id, const QString &label, const QString &status=QString(), bool canBeCanceled=true, KPIM::ProgressItem::CryptoStatus cryptoStatus=KPIM::ProgressItem::Unencrypted)
void setCompression(Compression c)
DeflateCompression
Q_SCRIPTABLE Q_NOREPLY void abort()
QString i18np(const char *singular, const char *plural, const TYPE &arg...)
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
Type type(const QSqlDatabase &db)
KCALUTILS_EXPORT QString errorMessage(const KCalendarCore::Exception &exception)
QString path(const QString &relativePath)
void information(QWidget *parent, const QString &text, const QString &title=QString(), const QString &dontShowAgainName=QString(), Options options=Notify)
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
The filter dialog.
qsizetype size() const const
void append(QList< T > &&value)
void clear()
bool isEmpty() const const
T takeAt(qsizetype i)
value_type takeFirst()
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
QVariant property(const char *name) const const
T qobject_cast(QObject *object)
bool setProperty(const char *name, QVariant &&value)
T * data() const const
QString & append(QChar ch)
bool isEmpty() const const
qsizetype lastIndexOf(QChar ch, Qt::CaseSensitivity cs) const const
QString number(double n, char format, int precision)
QString & prepend(QChar ch)
void truncate(qsizetype position)
bool isEmpty() const const
QString path(ComponentFormattingOptions options) const const
QString toString() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:09:01 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.