Akonadi

trashjob.cpp
1 /*
2  SPDX-FileCopyrightText: 2011 Christian Mollekopf <[email protected]>
3 
4  SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "trashjob.h"
8 
9 #include "entitydeletedattribute.h"
10 #include "job_p.h"
11 #include "trashsettings.h"
12 
13 #include <KLocalizedString>
14 
15 #include "collectiondeletejob.h"
16 #include "collectionfetchjob.h"
17 #include "collectionfetchscope.h"
18 #include "collectionmodifyjob.h"
19 #include "collectionmovejob.h"
20 #include "itemdeletejob.h"
21 #include "itemfetchjob.h"
22 #include "itemfetchscope.h"
23 #include "itemmodifyjob.h"
24 #include "itemmovejob.h"
25 
26 #include "akonadicore_debug.h"
27 
28 #include <QHash>
29 
30 using namespace Akonadi;
31 
32 class Akonadi::TrashJobPrivate : public JobPrivate
33 {
34 public:
35  explicit TrashJobPrivate(TrashJob *parent)
36  : JobPrivate(parent)
37  {
38  }
39  // 4.
40  void selectResult(KJob *job);
41  // 3.
42  // Helper functions to recursively set the attribute on deleted collections
43  void setAttribute(const Akonadi::Collection::List & /*list*/);
44  void setAttribute(const Akonadi::Item::List & /*list*/);
45  // Set attributes after ensuring that move job was successful
46  void setAttribute(KJob *job);
47 
48  // 2.
49  // called after parent of the trashed item was fetched (needed to see in which resource the item is in)
50  void parentCollectionReceived(const Akonadi::Collection::List & /*collections*/);
51 
52  // 1.
53  // called after initial fetch of trashed items
54  void itemsReceived(const Akonadi::Item::List & /*items*/);
55  // called after initial fetch of trashed collection
56  void collectionsReceived(const Akonadi::Collection::List & /*collections*/);
57 
58  Q_DECLARE_PUBLIC(TrashJob)
59 
60  Item::List mItems;
61  Collection mCollection;
62  Collection mRestoreCollection;
63  Collection mTrashCollection;
64  bool mKeepTrashInCollection = false;
65  bool mSetRestoreCollection = false; // only set restore collection when moved to trash collection (not in place)
66  bool mDeleteIfInTrash = false;
67  QHash<Collection, Item::List> mCollectionItems; // list of trashed items sorted according to parent collection
68  QHash<Item::Id, Collection> mParentCollections; // fetched parent collection of items (containing the resource name)
69 };
70 
71 void TrashJobPrivate::selectResult(KJob *job)
72 {
73  Q_Q(TrashJob);
74  if (job->error()) {
75  qCWarning(AKONADICORE_LOG) << job->objectName();
76  qCWarning(AKONADICORE_LOG) << job->errorString();
77  return; // KCompositeJob takes care of errors
78  }
79 
80  if (!q->hasSubjobs() || (q->subjobs().contains(static_cast<KJob *>(q->sender())) && q->subjobs().size() == 1)) {
81  q->emitResult();
82  }
83 }
84 
85 void TrashJobPrivate::setAttribute(const Akonadi::Collection::List &list)
86 {
87  Q_Q(TrashJob);
88 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
90 #else
92 #endif
93  while (i.hasNext()) {
94  const Collection &col = i.next();
95  auto eda = new EntityDeletedAttribute();
96  if (mSetRestoreCollection) {
97  Q_ASSERT(mRestoreCollection.isValid());
98  eda->setRestoreCollection(mRestoreCollection);
99  }
100 
101  Collection modCol(col.id()); // really only modify attribute (forget old remote ids, etc.), otherwise we have an error because of the move
102  modCol.addAttribute(eda);
103 
104  auto job = new CollectionModifyJob(modCol, q);
105  q->connect(job, &KJob::result, q, [this](KJob *job) {
106  selectResult(job);
107  });
108 
109  auto itemFetchJob = new ItemFetchJob(col, q);
110  // TODO not sure if it is guaranteed that itemsReceived is always before result (otherwise the result is emitted before the attributes are set)
111  q->connect(itemFetchJob, &ItemFetchJob::itemsReceived, q, [this](const auto &items) {
112  setAttribute(items);
113  });
114  q->connect(itemFetchJob, &KJob::result, q, [this](KJob *job) {
115  selectResult(job);
116  });
117  }
118 }
119 
120 void TrashJobPrivate::setAttribute(const Akonadi::Item::List &list)
121 {
122  Q_Q(TrashJob);
123  Item::List items = list;
124 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
126 #else
128 #endif
129  while (i.hasNext()) {
130  const Item &item = i.next();
131  auto eda = new EntityDeletedAttribute();
132  if (mSetRestoreCollection) {
133  // When deleting a collection, we want to restore the deleted collection's items restored to the deleted collection's parent, not the items parent
134  if (mRestoreCollection.isValid()) {
135  eda->setRestoreCollection(mRestoreCollection);
136  } else {
137  Q_ASSERT(mParentCollections.contains(item.parentCollection().id()));
138  eda->setRestoreCollection(mParentCollections.value(item.parentCollection().id()));
139  }
140  }
141 
142  Item modItem(item.id()); // really only modify attribute (forget old remote ids, etc.)
143  modItem.addAttribute(eda);
144  auto job = new ItemModifyJob(modItem, q);
145  job->setIgnorePayload(true);
146  q->connect(job, &KJob::result, q, [this](KJob *job) {
147  selectResult(job);
148  });
149  }
150 
151  // For some reason it is not possible to apply this change to multiple items at once
152  /*ItemModifyJob *job = new ItemModifyJob(items, q);
153  q->connect( job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)) );*/
154 }
155 
156 void TrashJobPrivate::setAttribute(KJob *job)
157 {
158  Q_Q(TrashJob);
159  if (job->error()) {
160  qCWarning(AKONADICORE_LOG) << job->objectName();
161  qCWarning(AKONADICORE_LOG) << job->errorString();
162  q->setError(Job::Unknown);
163  q->setErrorText(i18n("Move to trash collection failed, aborting trash operation"));
164  return;
165  }
166 
167  // For Items
168  const QVariant var = job->property("MovedItems");
169  if (var.isValid()) {
170  int id = var.toInt();
171  Q_ASSERT(id >= 0);
172  setAttribute(mCollectionItems.value(Collection(id)));
173  return;
174  }
175 
176  // For a collection
177  Q_ASSERT(mCollection.isValid());
178  setAttribute(Collection::List() << mCollection);
179  // Set the attribute on all subcollections and items
180  auto colFetchJob = new CollectionFetchJob(mCollection, CollectionFetchJob::Recursive, q);
181  q->connect(colFetchJob, &CollectionFetchJob::collectionsReceived, q, [this](const auto &cols) {
182  setAttribute(cols);
183  });
184  q->connect(colFetchJob, &KJob::result, q, [this](KJob *job) {
185  selectResult(job);
186  });
187 }
188 
189 void TrashJobPrivate::parentCollectionReceived(const Akonadi::Collection::List &collections)
190 {
191  Q_Q(TrashJob);
192  Q_ASSERT(collections.size() == 1);
193  const Collection &parentCollection = collections.first();
194 
195  // store attribute
196  Q_ASSERT(!parentCollection.resource().isEmpty());
197  Collection trashCollection = mTrashCollection;
198  if (!mTrashCollection.isValid()) {
199  trashCollection = TrashSettings::getTrashCollection(parentCollection.resource());
200  }
201  if (!mKeepTrashInCollection && trashCollection.isValid()) { // Only set the restore collection if the item is moved to trash
202  mSetRestoreCollection = true;
203  }
204 
205  mParentCollections.insert(parentCollection.id(), parentCollection);
206 
207  if (trashCollection.isValid()) { // Move the items to the correct collection if available
208  auto job = new ItemMoveJob(mCollectionItems.value(parentCollection), trashCollection, q);
209  job->setProperty("MovedItems", parentCollection.id());
210  q->connect(job, &KJob::result, q, [this](KJob *job) {
211  setAttribute(job);
212  }); // Wait until the move finished to set the attribute
213  q->connect(job, &KJob::result, q, [this](KJob *job) {
214  selectResult(job);
215  });
216  } else {
217  setAttribute(mCollectionItems.value(parentCollection));
218  }
219 }
220 
221 void TrashJobPrivate::itemsReceived(const Akonadi::Item::List &items)
222 {
223  Q_Q(TrashJob);
224  if (items.isEmpty()) {
225  q->setError(Job::Unknown);
226  q->setErrorText(i18n("Invalid items passed"));
227  q->emitResult();
228  return;
229  }
230 
231  Item::List toDelete;
232 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
233  QVectorIterator<Item> i(items);
234 #else
235  QListIterator<Item> i(items);
236 #endif
237  while (i.hasNext()) {
238  const Item &item = i.next();
239  if (item.hasAttribute<EntityDeletedAttribute>()) {
240  toDelete.append(item);
241  continue;
242  }
243  Q_ASSERT(item.parentCollection().isValid());
244  mCollectionItems[item.parentCollection()].append(item); // Sort by parent col ( = restore collection)
245  }
246 
247  for (auto it = mCollectionItems.cbegin(), e = mCollectionItems.cend(); it != e; ++it) {
248  auto job = new CollectionFetchJob(it.key(), Akonadi::CollectionFetchJob::Base, q);
249  q->connect(job, &CollectionFetchJob::collectionsReceived, q, [this](const auto &cols) {
250  parentCollectionReceived(cols);
251  });
252  }
253 
254  if (mDeleteIfInTrash && !toDelete.isEmpty()) {
255  auto job = new ItemDeleteJob(toDelete, q);
256  q->connect(job, &KJob::result, q, [this](KJob *job) {
257  selectResult(job);
258  });
259  } else if (mCollectionItems.isEmpty()) { // No job started, so we abort the job
260  qCWarning(AKONADICORE_LOG) << "Nothing to do";
261  q->emitResult();
262  }
263 }
264 
265 void TrashJobPrivate::collectionsReceived(const Akonadi::Collection::List &collections)
266 {
267  Q_Q(TrashJob);
268  if (collections.isEmpty()) {
269  q->setError(Job::Unknown);
270  q->setErrorText(i18n("Invalid collection passed"));
271  q->emitResult();
272  return;
273  }
274  Q_ASSERT(collections.size() == 1);
275  mCollection = collections.first();
276 
277  if (mCollection.hasAttribute<EntityDeletedAttribute>()) { // marked as deleted
278  if (mDeleteIfInTrash) {
279  auto job = new CollectionDeleteJob(mCollection, q);
280  q->connect(job, &KJob::result, q, [this](KJob *job) {
281  selectResult(job);
282  });
283  } else {
284  qCWarning(AKONADICORE_LOG) << "Nothing to do";
285  q->emitResult();
286  }
287  return;
288  }
289 
290  Collection trashCollection = mTrashCollection;
291  if (!mTrashCollection.isValid()) {
292  trashCollection = TrashSettings::getTrashCollection(mCollection.resource());
293  }
294  if (!mKeepTrashInCollection && trashCollection.isValid()) { // only set the restore collection if the item is moved to trash
295  mSetRestoreCollection = true;
296  Q_ASSERT(mCollection.parentCollection().isValid());
297  mRestoreCollection = mCollection.parentCollection();
298  mRestoreCollection.setResource(mCollection.resource()); // The parent collection doesn't contain the resource, so we have to set it manually
299  }
300 
301  if (trashCollection.isValid()) {
302  auto job = new CollectionMoveJob(mCollection, trashCollection, q);
303  q->connect(job, &KJob::result, q, [this](KJob *job) {
304  setAttribute(job);
305  });
306  q->connect(job, &KJob::result, q, [this](KJob *job) {
307  selectResult(job);
308  });
309  } else {
310  setAttribute(Collection::List() << mCollection);
311  }
312 }
313 
314 TrashJob::TrashJob(const Item &item, QObject *parent)
315  : Job(new TrashJobPrivate(this), parent)
316 {
317  Q_D(TrashJob);
318  d->mItems << item;
319 }
320 
321 TrashJob::TrashJob(const Item::List &items, QObject *parent)
322  : Job(new TrashJobPrivate(this), parent)
323 {
324  Q_D(TrashJob);
325  d->mItems = items;
326 }
327 
328 TrashJob::TrashJob(const Collection &collection, QObject *parent)
329  : Job(new TrashJobPrivate(this), parent)
330 {
331  Q_D(TrashJob);
332  d->mCollection = collection;
333 }
334 
335 TrashJob::~TrashJob()
336 {
337 }
338 
339 Item::List TrashJob::items() const
340 {
341  Q_D(const TrashJob);
342  return d->mItems;
343 }
344 
346 {
347  Q_D(TrashJob);
348  d->mTrashCollection = collection;
349 }
350 
352 {
353  Q_D(TrashJob);
354  d->mKeepTrashInCollection = enable;
355 }
356 
357 void TrashJob::deleteIfInTrash(bool enable)
358 {
359  Q_D(TrashJob);
360  d->mDeleteIfInTrash = enable;
361 }
362 
364 {
365  Q_D(TrashJob);
366 
367  // Fetch items first to ensure that the EntityDeletedAttribute is available
368  if (!d->mItems.isEmpty()) {
369  auto job = new ItemFetchJob(d->mItems, this);
370  job->fetchScope().setAncestorRetrieval(Akonadi::ItemFetchScope::Parent); // so we have access to the resource
371  // job->fetchScope().setCacheOnly(true);
372  job->fetchScope().fetchAttribute<EntityDeletedAttribute>(true);
373  connect(job, &ItemFetchJob::itemsReceived, this, [d](const auto &items) {
374  d->itemsReceived(items);
375  });
376 
377  } else if (d->mCollection.isValid()) {
378  auto job = new CollectionFetchJob(d->mCollection, CollectionFetchJob::Base, this);
379  job->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::Parent);
380  connect(job, &CollectionFetchJob::collectionsReceived, this, [d](const auto &cols) {
381  d->collectionsReceived(cols);
382  });
383 
384  } else {
385  qCWarning(AKONADICORE_LOG) << "No valid collection or empty itemlist";
387  setErrorText(i18n("No valid collection or empty itemlist"));
388  emitResult();
389  }
390 }
391 
392 #include "moc_trashjob.cpp"
@ Parent
Only retrieve the immediate parent collection.
void itemsReceived(const Akonadi::Item::List &items)
This signal is emitted whenever new items have been fetched completely.
bool isEmpty() const const
bool isValid() const const
Job that modifies a collection in the Akonadi storage.
Job that deletes items from the Akonadi storage.
Definition: itemdeletejob.h:47
void deleteIfInTrash(bool enable)
Delete Items which are already in trash, instead of ignoring them.
Definition: trashjob.cpp:357
@ Unknown
Unknown error.
Definition: job.h:102
void setErrorText(const QString &errorText)
void result(KJob *job)
Represents a collection of PIM items.
Definition: collection.h:61
Job that moves items/collection to trash.
Definition: trashjob.h:52
KIOFILEWIDGETS_EXPORT QStringList list(const QString &fileClass)
T & first()
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
TrashJob(const Item &item, QObject *parent=nullptr)
Creates a new trash job that marks item as trash, and moves it to the configured trash collection.
Definition: trashjob.cpp:314
QVector< Item > List
Describes a list of items.
Definition: item.h:116
void setTrashCollection(const Collection &trashcollection)
Moves all entities to the give collection.
Definition: trashjob.cpp:345
Job that fetches collections from the Akonadi storage.
Job that moves an item into a different collection in the Akonadi storage.
Definition: itemmovejob.h:34
void collectionsReceived(const Akonadi::Collection::List &collections)
This signal is emitted whenever the job has received collections.
Job that modifies an existing item in the Akonadi storage.
Definition: itemmodifyjob.h:81
QString i18n(const char *text, const TYPE &arg...)
bool isEmpty() const const
Collection parentCollection() const
Returns the parent collection of this object.
Definition: item.cpp:153
int toInt(bool *ok) const const
@ Base
Only fetch the base collection.
Base class for all actions in the Akonadi storage.
Definition: job.h:80
Job that moves a collection in the Akonadi storage to a new parent collection.
bool setProperty(const char *name, const QVariant &value)
void keepTrashInCollection(bool enable)
Ignore configured Trash collections and keep all items local.
Definition: trashjob.cpp:351
void doStart() override
This method must be reimplemented in the concrete jobs.
Definition: trashjob.cpp:363
AKONADICORE_EXPORT Collection getTrashCollection(const QString &resource)
Get the trash collection for the given resource.
Id id() const
Returns the unique identifier of the item.
Definition: item.cpp:63
Job that deletes a collection in the Akonadi storage.
bool hasAttribute(const QByteArray &name) const
Returns true if the item has an attribute of the given type name, false otherwise.
Definition: item.cpp:128
An Attribute that marks that an entity was marked as deleted.
int size() const const
Job that fetches items from the Akonadi storage.
Definition: itemfetchjob.h:69
@ Recursive
List all sub-collections.
void emitResult()
virtual QString errorString() const
int error() const
void setError(int errorCode)
Represents a PIM item stored in Akonadi storage.
Definition: item.h:105
Q_D(Todo)
QVariant property(const char *name) const const
@ Parent
Only retrieve the immediate parent collection.
Helper integration between Akonadi and Qt.
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Tue Dec 6 2022 03:53:34 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.