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);
89  while (i.hasNext()) {
90  const Collection &col = i.next();
91  auto eda = new EntityDeletedAttribute();
92  if (mSetRestoreCollection) {
93  Q_ASSERT(mRestoreCollection.isValid());
94  eda->setRestoreCollection(mRestoreCollection);
95  }
96 
97  Collection modCol(col.id()); // really only modify attribute (forget old remote ids, etc.), otherwise we have an error because of the move
98  modCol.addAttribute(eda);
99 
100  auto job = new CollectionModifyJob(modCol, q);
101  q->connect(job, &KJob::result, q, [this](KJob *job) {
102  selectResult(job);
103  });
104 
105  auto itemFetchJob = new ItemFetchJob(col, q);
106  // TODO not sure if it is guaranteed that itemsReceived is always before result (otherwise the result is emitted before the attributes are set)
107  q->connect(itemFetchJob, &ItemFetchJob::itemsReceived, q, [this](const auto &items) {
108  setAttribute(items);
109  });
110  q->connect(itemFetchJob, &KJob::result, q, [this](KJob *job) {
111  selectResult(job);
112  });
113  }
114 }
115 
116 void TrashJobPrivate::setAttribute(const Akonadi::Item::List &list)
117 {
118  Q_Q(TrashJob);
119  Item::List items = list;
121  while (i.hasNext()) {
122  const Item &item = i.next();
123  auto eda = new EntityDeletedAttribute();
124  if (mSetRestoreCollection) {
125  // When deleting a collection, we want to restore the deleted collection's items restored to the deleted collection's parent, not the items parent
126  if (mRestoreCollection.isValid()) {
127  eda->setRestoreCollection(mRestoreCollection);
128  } else {
129  Q_ASSERT(mParentCollections.contains(item.parentCollection().id()));
130  eda->setRestoreCollection(mParentCollections.value(item.parentCollection().id()));
131  }
132  }
133 
134  Item modItem(item.id()); // really only modify attribute (forget old remote ids, etc.)
135  modItem.addAttribute(eda);
136  auto job = new ItemModifyJob(modItem, q);
137  job->setIgnorePayload(true);
138  q->connect(job, &KJob::result, q, [this](KJob *job) {
139  selectResult(job);
140  });
141  }
142 
143  // For some reason it is not possible to apply this change to multiple items at once
144  /*ItemModifyJob *job = new ItemModifyJob(items, q);
145  q->connect( job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)) );*/
146 }
147 
148 void TrashJobPrivate::setAttribute(KJob *job)
149 {
150  Q_Q(TrashJob);
151  if (job->error()) {
152  qCWarning(AKONADICORE_LOG) << job->objectName();
153  qCWarning(AKONADICORE_LOG) << job->errorString();
154  q->setError(Job::Unknown);
155  q->setErrorText(i18n("Move to trash collection failed, aborting trash operation"));
156  return;
157  }
158 
159  // For Items
160  const QVariant var = job->property("MovedItems");
161  if (var.isValid()) {
162  int id = var.toInt();
163  Q_ASSERT(id >= 0);
164  setAttribute(mCollectionItems.value(Collection(id)));
165  return;
166  }
167 
168  // For a collection
169  Q_ASSERT(mCollection.isValid());
170  setAttribute(Collection::List() << mCollection);
171  // Set the attribute on all subcollections and items
172  auto colFetchJob = new CollectionFetchJob(mCollection, CollectionFetchJob::Recursive, q);
173  q->connect(colFetchJob, &CollectionFetchJob::collectionsReceived, q, [this](const auto &cols) {
174  setAttribute(cols);
175  });
176  q->connect(colFetchJob, &KJob::result, q, [this](KJob *job) {
177  selectResult(job);
178  });
179 }
180 
181 void TrashJobPrivate::parentCollectionReceived(const Akonadi::Collection::List &collections)
182 {
183  Q_Q(TrashJob);
184  Q_ASSERT(collections.size() == 1);
185  const Collection &parentCollection = collections.first();
186 
187  // store attribute
188  Q_ASSERT(!parentCollection.resource().isEmpty());
189  Collection trashCollection = mTrashCollection;
190  if (!mTrashCollection.isValid()) {
191  trashCollection = TrashSettings::getTrashCollection(parentCollection.resource());
192  }
193  if (!mKeepTrashInCollection && trashCollection.isValid()) { // Only set the restore collection if the item is moved to trash
194  mSetRestoreCollection = true;
195  }
196 
197  mParentCollections.insert(parentCollection.id(), parentCollection);
198 
199  if (trashCollection.isValid()) { // Move the items to the correct collection if available
200  auto job = new ItemMoveJob(mCollectionItems.value(parentCollection), trashCollection, q);
201  job->setProperty("MovedItems", parentCollection.id());
202  q->connect(job, &KJob::result, q, [this](KJob *job) {
203  setAttribute(job);
204  }); // Wait until the move finished to set the attribute
205  q->connect(job, &KJob::result, q, [this](KJob *job) {
206  selectResult(job);
207  });
208  } else {
209  setAttribute(mCollectionItems.value(parentCollection));
210  }
211 }
212 
213 void TrashJobPrivate::itemsReceived(const Akonadi::Item::List &items)
214 {
215  Q_Q(TrashJob);
216  if (items.isEmpty()) {
217  q->setError(Job::Unknown);
218  q->setErrorText(i18n("Invalid items passed"));
219  q->emitResult();
220  return;
221  }
222 
223  Item::List toDelete;
224  QListIterator<Item> i(items);
225  while (i.hasNext()) {
226  const Item &item = i.next();
227  if (item.hasAttribute<EntityDeletedAttribute>()) {
228  toDelete.append(item);
229  continue;
230  }
231  Q_ASSERT(item.parentCollection().isValid());
232  mCollectionItems[item.parentCollection()].append(item); // Sort by parent col ( = restore collection)
233  }
234 
235  for (auto it = mCollectionItems.cbegin(), e = mCollectionItems.cend(); it != e; ++it) {
236  auto job = new CollectionFetchJob(it.key(), Akonadi::CollectionFetchJob::Base, q);
237  q->connect(job, &CollectionFetchJob::collectionsReceived, q, [this](const auto &cols) {
238  parentCollectionReceived(cols);
239  });
240  }
241 
242  if (mDeleteIfInTrash && !toDelete.isEmpty()) {
243  auto job = new ItemDeleteJob(toDelete, q);
244  q->connect(job, &KJob::result, q, [this](KJob *job) {
245  selectResult(job);
246  });
247  } else if (mCollectionItems.isEmpty()) { // No job started, so we abort the job
248  qCWarning(AKONADICORE_LOG) << "Nothing to do";
249  q->emitResult();
250  }
251 }
252 
253 void TrashJobPrivate::collectionsReceived(const Akonadi::Collection::List &collections)
254 {
255  Q_Q(TrashJob);
256  if (collections.isEmpty()) {
257  q->setError(Job::Unknown);
258  q->setErrorText(i18n("Invalid collection passed"));
259  q->emitResult();
260  return;
261  }
262  Q_ASSERT(collections.size() == 1);
263  mCollection = collections.first();
264 
265  if (mCollection.hasAttribute<EntityDeletedAttribute>()) { // marked as deleted
266  if (mDeleteIfInTrash) {
267  auto job = new CollectionDeleteJob(mCollection, q);
268  q->connect(job, &KJob::result, q, [this](KJob *job) {
269  selectResult(job);
270  });
271  } else {
272  qCWarning(AKONADICORE_LOG) << "Nothing to do";
273  q->emitResult();
274  }
275  return;
276  }
277 
278  Collection trashCollection = mTrashCollection;
279  if (!mTrashCollection.isValid()) {
280  trashCollection = TrashSettings::getTrashCollection(mCollection.resource());
281  }
282  if (!mKeepTrashInCollection && trashCollection.isValid()) { // only set the restore collection if the item is moved to trash
283  mSetRestoreCollection = true;
284  Q_ASSERT(mCollection.parentCollection().isValid());
285  mRestoreCollection = mCollection.parentCollection();
286  mRestoreCollection.setResource(mCollection.resource()); // The parent collection doesn't contain the resource, so we have to set it manually
287  }
288 
289  if (trashCollection.isValid()) {
290  auto job = new CollectionMoveJob(mCollection, trashCollection, q);
291  q->connect(job, &KJob::result, q, [this](KJob *job) {
292  setAttribute(job);
293  });
294  q->connect(job, &KJob::result, q, [this](KJob *job) {
295  selectResult(job);
296  });
297  } else {
298  setAttribute(Collection::List() << mCollection);
299  }
300 }
301 
302 TrashJob::TrashJob(const Item &item, QObject *parent)
303  : Job(new TrashJobPrivate(this), parent)
304 {
305  Q_D(TrashJob);
306  d->mItems << item;
307 }
308 
309 TrashJob::TrashJob(const Item::List &items, QObject *parent)
310  : Job(new TrashJobPrivate(this), parent)
311 {
312  Q_D(TrashJob);
313  d->mItems = items;
314 }
315 
316 TrashJob::TrashJob(const Collection &collection, QObject *parent)
317  : Job(new TrashJobPrivate(this), parent)
318 {
319  Q_D(TrashJob);
320  d->mCollection = collection;
321 }
322 
323 TrashJob::~TrashJob()
324 {
325 }
326 
327 Item::List TrashJob::items() const
328 {
329  Q_D(const TrashJob);
330  return d->mItems;
331 }
332 
334 {
335  Q_D(TrashJob);
336  d->mTrashCollection = collection;
337 }
338 
340 {
341  Q_D(TrashJob);
342  d->mKeepTrashInCollection = enable;
343 }
344 
345 void TrashJob::deleteIfInTrash(bool enable)
346 {
347  Q_D(TrashJob);
348  d->mDeleteIfInTrash = enable;
349 }
350 
352 {
353  Q_D(TrashJob);
354 
355  // Fetch items first to ensure that the EntityDeletedAttribute is available
356  if (!d->mItems.isEmpty()) {
357  auto job = new ItemFetchJob(d->mItems, this);
358  job->fetchScope().setAncestorRetrieval(Akonadi::ItemFetchScope::Parent); // so we have access to the resource
359  // job->fetchScope().setCacheOnly(true);
360  job->fetchScope().fetchAttribute<EntityDeletedAttribute>(true);
361  connect(job, &ItemFetchJob::itemsReceived, this, [d](const auto &items) {
362  d->itemsReceived(items);
363  });
364 
365  } else if (d->mCollection.isValid()) {
366  auto job = new CollectionFetchJob(d->mCollection, CollectionFetchJob::Base, this);
367  job->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::Parent);
368  connect(job, &CollectionFetchJob::collectionsReceived, this, [d](const auto &cols) {
369  d->collectionsReceived(cols);
370  });
371 
372  } else {
373  qCWarning(AKONADICORE_LOG) << "No valid collection or empty itemlist";
375  setErrorText(i18n("No valid collection or empty itemlist"));
376  emitResult();
377  }
378 }
379 
380 #include "moc_trashjob.cpp"
T & first()
@ 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 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:345
@ 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)
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:302
void setTrashCollection(const Collection &trashcollection)
Moves all entities to the give collection.
Definition: trashjob.cpp:333
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.
int size() const const
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
bool isEmpty() 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:339
void doStart() override
This method must be reimplemented in the concrete jobs.
Definition: trashjob.cpp:351
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.
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:100
Q_D(Todo)
QList< Item > List
Describes a list of items.
Definition: item.h:111
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-2023 The KDE developers.
Generated on Wed Sep 27 2023 04:01:20 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.