Mailcommon

backupjob.cpp
1 /*
2 
3  SPDX-FileCopyrightText: 2009 Klarälvdalens Datakonsult AB, a KDAB Group company <[email protected]>
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 <KIO/Global>
21 #include <KLocalizedString>
22 #include <KMessageBox>
23 #include <KTar>
24 #include <KZip>
25 
26 #include <QFileInfo>
27 #include <QTimer>
28 
29 using namespace MailCommon;
30 static const mode_t archivePerms = S_IFREG | 0644;
31 
32 BackupJob::BackupJob(QWidget *parent)
33  : QObject(parent)
34  , mArchiveTime(QDateTime::currentDateTime())
35  , mRootFolder(0)
36  , mParentWidget(parent)
37  , mCurrentFolder(Akonadi::Collection())
38 {
39 }
40 
41 BackupJob::~BackupJob()
42 {
43  mPendingFolders.clear();
44  delete mArchive;
45  mArchive = nullptr;
46 }
47 
48 void BackupJob::setRootFolder(const Akonadi::Collection &rootFolder)
49 {
50  mRootFolder = rootFolder;
51 }
52 
53 void BackupJob::setRealPath(const QString &path)
54 {
55  mRealPath = path;
56 }
57 
58 void BackupJob::setSaveLocation(const QUrl &savePath)
59 {
60  mMailArchivePath = savePath;
61 }
62 
63 void BackupJob::setArchiveType(ArchiveType type)
64 {
65  mArchiveType = type;
66 }
67 
68 void BackupJob::setDeleteFoldersAfterCompletion(bool deleteThem)
69 {
70  mDeleteFoldersAfterCompletion = deleteThem;
71 }
72 
73 void BackupJob::setRecursive(bool recursive)
74 {
75  mRecursive = recursive;
76 }
77 
78 bool 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 
109 bool 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 
119 void BackupJob::cancelJob()
120 {
121  abort(i18n("The operation was canceled by the user."));
122 }
123 
124 void 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, i18n("Archiving failed"));
156  }
157  deleteLater();
158  // Clean up archive file here?
159 }
160 
161 void 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  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  text += QLatin1Char('\n')
186  + i18np("1 message of size %2 was archived.",
187  "%1 messages with the total size of %2 were archived.",
188  mArchivedMessages,
189  KIO::convertSize(mArchivedSize));
190  text += QLatin1Char('\n') + i18n("The archive file has a size of %1.", KIO::convertSize(archiveFileInfo.size()));
191  if (mDisplayMessageBox) {
192  KMessageBox::information(mParentWidget, text, i18n("Archiving finished"));
193  }
194 
195  if (mDeleteFoldersAfterCompletion) {
196  // Some safety checks first...
197  if (archiveFileInfo.exists() && (mArchivedSize > 0 || mArchivedMessages == 0)) {
198  // Sorry for any data loss!
199  new Akonadi::CollectionDeleteJob(mRootFolder);
200  }
201  }
202  Q_EMIT backupDone(text);
203  deleteLater();
204 }
205 
206 void BackupJob::archiveNextMessage()
207 {
208  if (mAborted) {
209  return;
210  }
211 
212  if (mPendingMessages.isEmpty()) {
213  qCDebug(MAILCOMMON_LOG) << "===> All messages done in folder " << mCurrentFolder.name();
214  archiveNextFolder();
215  return;
216  }
217 
218  const Akonadi::Item item = mPendingMessages.takeFirst();
219  qCDebug(MAILCOMMON_LOG) << "Fetching item with ID" << item.id() << "for folder" << mCurrentFolder.name();
220 
221  mCurrentJob = new Akonadi::ItemFetchJob(item);
222  mCurrentJob->fetchScope().fetchFullPayload(true);
223  connect(mCurrentJob, &Akonadi::ItemFetchJob::result, this, &BackupJob::itemFetchJobResult);
224 }
225 
226 void BackupJob::processMessage(const Akonadi::Item &item)
227 {
228  if (mAborted) {
229  return;
230  }
231 
232  const auto message = item.payload<KMime::Message::Ptr>();
233  qCDebug(MAILCOMMON_LOG) << "Processing message with subject " << message->subject(false);
234  const QByteArray messageData = message->encodedContent();
235  const qint64 messageSize = messageData.size();
236  const QString messageName = QString::number(item.id());
237  const QString fileName = pathForCollection(mCurrentFolder) + QLatin1String("/cur/") + messageName;
238 
239  // PORT ME: user and group!
240  qCDebug(MAILCOMMON_LOG) << "AKONDI PORT: disabled code here!";
241  if (!mArchive->writeFile(fileName, messageData, archivePerms, QStringLiteral("user"), QStringLiteral("group"), mArchiveTime, mArchiveTime, mArchiveTime)) {
242  abort(i18n("Failed to write a message into the archive folder '%1'.", mCurrentFolder.name()));
243  return;
244  }
245 
246  ++mArchivedMessages;
247  mArchivedSize += messageSize;
248 
249  // Use a singleshot timer, otherwise the job started in archiveNextMessage()
250  // will hang
251  QTimer::singleShot(0, this, &BackupJob::archiveNextMessage);
252 }
253 
254 void BackupJob::itemFetchJobResult(KJob *job)
255 {
256  if (mAborted) {
257  return;
258  }
259 
260  Q_ASSERT(job == mCurrentJob);
261  mCurrentJob = nullptr;
262 
263  if (job->error()) {
264  Q_ASSERT(mCurrentFolder.isValid());
265  qCWarning(MAILCOMMON_LOG) << job->errorString();
266  abort(i18n("Downloading a message in folder '%1' failed.", mCurrentFolder.name()));
267  } else {
268  auto fetchJob = qobject_cast<Akonadi::ItemFetchJob *>(job);
269  Q_ASSERT(fetchJob);
270  Q_ASSERT(fetchJob->items().size() == 1);
271  processMessage(fetchJob->items().constFirst());
272  }
273 }
274 
275 bool BackupJob::writeDirHelper(const QString &directoryPath)
276 {
277  // PORT ME: Correct user/group
278  qCDebug(MAILCOMMON_LOG) << "AKONDI PORT: Disabled code here!";
279  return mArchive->writeDir(directoryPath, QStringLiteral("user"), QStringLiteral("group"), 040755, mArchiveTime, mArchiveTime, mArchiveTime);
280 }
281 
282 QString BackupJob::collectionName(const Akonadi::Collection &collection) const
283 {
284  for (const Akonadi::Collection &curCol : std::as_const(mAllFolders)) {
285  if (curCol == collection) {
286  return curCol.name();
287  }
288  }
289  Q_ASSERT(false);
290  return {};
291 }
292 
293 QString BackupJob::pathForCollection(const Akonadi::Collection &collection) const
294 {
295  QString fullPath = collectionName(collection);
296  Akonadi::Collection curCol = collection.parentCollection();
297  if (collection != mRootFolder) {
298  Q_ASSERT(curCol.isValid());
299  while (curCol != mRootFolder) {
300  fullPath.prepend(QLatin1Char('.') + collectionName(curCol) + QLatin1String(".directory/"));
301  curCol = curCol.parentCollection();
302  }
303  Q_ASSERT(curCol == mRootFolder);
304  fullPath.prepend(QLatin1Char('.') + collectionName(curCol) + QLatin1String(".directory/"));
305  }
306  return fullPath;
307 }
308 
309 QString BackupJob::subdirPathForCollection(const Akonadi::Collection &collection) const
310 {
311  QString path = pathForCollection(collection);
312  const int parentDirEndIndex = path.lastIndexOf(collection.name());
313  Q_ASSERT(parentDirEndIndex != -1);
314  path.truncate(parentDirEndIndex);
315  path.append(QLatin1Char('.') + collection.name() + QLatin1String(".directory"));
316  return path;
317 }
318 
319 void BackupJob::archiveNextFolder()
320 {
321  if (mAborted) {
322  return;
323  }
324 
325  if (mPendingFolders.isEmpty()) {
326  finish();
327  return;
328  }
329 
330  mCurrentFolder = mPendingFolders.takeAt(0);
331  qCDebug(MAILCOMMON_LOG) << "===> Archiving next folder: " << mCurrentFolder.name();
332  const QString archivingStr(i18n("Archiving folder %1", mCurrentFolder.name()));
333  if (mProgressItem) {
334  mProgressItem->setStatus(archivingStr);
335  }
336  PimCommon::BroadcastStatus::instance()->setStatusMsg(archivingStr);
337 
338  const QString folderName = mCurrentFolder.name();
339  bool success = true;
340  if (hasChildren(mCurrentFolder)) {
341  if (!writeDirHelper(subdirPathForCollection(mCurrentFolder))) {
342  success = false;
343  }
344  }
345  if (success) {
346  if (!writeDirHelper(pathForCollection(mCurrentFolder))) {
347  success = false;
348  } else if (!writeDirHelper(pathForCollection(mCurrentFolder) + QLatin1String("/cur"))) {
349  success = false;
350  } else if (!writeDirHelper(pathForCollection(mCurrentFolder) + QLatin1String("/new"))) {
351  success = false;
352  } else if (!writeDirHelper(pathForCollection(mCurrentFolder) + QLatin1String("/tmp"))) {
353  success = false;
354  }
355  }
356  if (!success) {
357  abort(i18n("Unable to create folder structure for folder '%1' within archive file.", mCurrentFolder.name()));
358  return;
359  }
360  auto job = new Akonadi::ItemFetchJob(mCurrentFolder);
361  job->setProperty("folderName", folderName);
362  connect(job, &Akonadi::ItemFetchJob::result, this, &BackupJob::onArchiveNextFolderDone);
363 }
364 
365 void BackupJob::onArchiveNextFolderDone(KJob *job)
366 {
367  if (job->error()) {
368  qCWarning(MAILCOMMON_LOG) << job->errorString();
369  abort(i18n("Unable to get message list for folder %1.", job->property("folderName").toString()));
370  return;
371  }
372 
373  auto fetchJob = qobject_cast<Akonadi::ItemFetchJob *>(job);
374  mPendingMessages += fetchJob->items();
375  archiveNextMessage();
376 }
377 
378 void BackupJob::start()
379 {
380  Q_ASSERT(!mMailArchivePath.isEmpty());
381  Q_ASSERT(mRootFolder.isValid());
382 
383  if (!queueFolders(mRootFolder)) {
384  return;
385  }
386 
387  switch (mArchiveType) {
388  case Zip: {
389  KZip *zip = new KZip(mMailArchivePath.path());
391  mArchive = zip;
392  break;
393  }
394  case Tar:
395  mArchive = new KTar(mMailArchivePath.path(), QStringLiteral("application/x-tar"));
396  break;
397  case TarGz:
398  mArchive = new KTar(mMailArchivePath.path(), QStringLiteral("application/x-gzip"));
399  break;
400  case TarBz2:
401  mArchive = new KTar(mMailArchivePath.path(), QStringLiteral("application/x-bzip2"));
402  break;
403  }
404 
405  qCDebug(MAILCOMMON_LOG) << "Starting backup.";
406  if (!mArchive->open(QIODevice::WriteOnly)) {
407  abort(i18n("Unable to open archive for writing."));
408  return;
409  }
410 
411  mProgressItem = KPIM::ProgressManager::createProgressItem(QStringLiteral("BackupJob"), i18n("Archiving"), QString(), true);
412  mProgressItem->setUsesBusyIndicator(true);
413  connect(mProgressItem.data(), &KPIM::ProgressItem::progressItemCanceled, this, &BackupJob::cancelJob);
414 
415  archiveNextFolder();
416 }
417 
418 void BackupJob::setDisplayMessageBox(bool display)
419 {
420  mDisplayMessageBox = display;
421 }
T takeAt(int i)
void truncate(int position)
bool isEmpty() const const
KIOCORE_EXPORT QString convertSize(KIO::filesize_t size)
QString number(int n, int base)
Q_EMITQ_EMIT
void result(KJob *job)
Type type(const QSqlDatabase &db)
void append(const T &value)
QString & prepend(QChar ch)
void fetchFullPayload(bool fetch=true)
bool kill(KillVerbosity verbosity=Quietly)
int lastIndexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
KCALUTILS_EXPORT QString errorMessage(const KCalendarCore::Exception &exception)
DeflateCompression
virtual bool close()
void deleteLater()
void setCompression(Compression c)
ItemFetchScope & fetchScope()
void clear()
QString i18n(const char *text, const TYPE &arg...)
bool isEmpty() const const
bool isEmpty() const const
Collection & parentCollection()
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())
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
virtual bool open(QIODevice::OpenMode mode)
T takeFirst()
bool isOpen() const
QString i18np(const char *singular, const char *plural, const TYPE &arg...)
bool setProperty(const char *name, const QVariant &value)
Id id() const
QString path(QUrl::ComponentFormattingOptions options) const const
void progressItemCanceled(KPIM::ProgressItem *)
bool isValid() const
int size() const const
void information(QWidget *parent, const QString &text, const QString &title=QString(), const QString &dontShowAgainName=QString(), Options options=Notify)
static ProgressItem * createProgressItem(const QString &id, const QString &label, const QString &status=QString(), bool canBeCanceled=true, KPIM::ProgressItem::CryptoStatus cryptoStatus=KPIM::ProgressItem::Unencrypted)
T * data() const const
virtual QString errorString() const
int error() const
QString message
QString & append(QChar ch)
The filter dialog.
QString toString() const const
QVariant property(const char *name) const const
bool writeFile(const QString &name, const QByteArray &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())
T payload() const
QString name() const
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Sat Sep 24 2022 03:58:14 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.