Akonadi

trashjob.cpp
1/*
2 SPDX-FileCopyrightText: 2011 Christian Mollekopf <chrigi_1@fastmail.fm>
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
30using namespace Akonadi;
31
32class Akonadi::TrashJobPrivate : public JobPrivate
33{
34public:
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
71void 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
85void TrashJobPrivate::setAttribute(const Akonadi::Collection::List &list)
86{
87 Q_Q(TrashJob);
88 QListIterator<Collection> i(list);
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
116void TrashJobPrivate::setAttribute(const Akonadi::Item::List &list)
117{
118 Q_Q(TrashJob);
119 Item::List items = list;
120 QMutableListIterator<Item> i(items);
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
148void 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
181void 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
213void 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
253void 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
303 : Job(new TrashJobPrivate(this), parent)
304{
305 Q_D(TrashJob);
306 d->mItems << item;
307}
308
310 : Job(new TrashJobPrivate(this), parent)
311{
312 Q_D(TrashJob);
313 d->mItems = items;
314}
315
317 : Job(new TrashJobPrivate(this), parent)
318{
319 Q_D(TrashJob);
320 d->mCollection = collection;
321}
322
323TrashJob::~TrashJob()
324{
325}
326
327Item::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
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"
Job that fetches collections from the Akonadi storage.
@ Recursive
List all sub-collections.
@ Base
Only fetch the base collection.
void collectionsReceived(const Akonadi::Collection::List &collections)
This signal is emitted whenever the job has received collections.
@ Parent
Only retrieve the immediate parent collection.
Represents a collection of PIM items.
Definition collection.h:62
QList< Collection > List
Describes a list of collections.
Definition collection.h:84
An Attribute that marks that an entity was marked as deleted.
Job that fetches items from the Akonadi storage.
void itemsReceived(const Akonadi::Item::List &items)
This signal is emitted whenever new items have been fetched completely.
@ Parent
Only retrieve the immediate parent collection.
Represents a PIM item stored in Akonadi storage.
Definition item.h:100
QList< Item > List
Describes a list of items.
Definition item.h:110
Job(QObject *parent=nullptr)
Creates a new job.
Definition job.cpp:290
@ Unknown
Unknown error.
Definition job.h:102
Job that moves items/collection to trash.
Definition trashjob.h:53
void deleteIfInTrash(bool enable)
Delete Items which are already in trash, instead of ignoring them.
Definition trashjob.cpp:345
void doStart() override
This method must be reimplemented in the concrete jobs.
Definition trashjob.cpp:351
void setTrashCollection(const Collection &trashcollection)
Moves all entities to the give collection.
Definition trashjob.cpp:333
void keepTrashInCollection(bool enable)
Ignore configured Trash collections and keep all items local.
Definition trashjob.cpp:339
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 setErrorText(const QString &errorText)
virtual QString errorString() const
void emitResult()
int error() const
void result(KJob *job)
void setError(int errorCode)
QString i18n(const char *text, const TYPE &arg...)
AKONADICORE_EXPORT Collection getTrashCollection(const QString &resource)
Get the trash collection for the given resource.
Helper integration between Akonadi and Qt.
KIOCORE_EXPORT QStringList list(const QString &fileClass)
void append(QList< T > &&value)
T & first()
bool isEmpty() const const
qsizetype size() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QObject * parent() const const
QVariant property(const char *name) const const
bool setProperty(const char *name, QVariant &&value)
bool isEmpty() const const
bool isValid() const const
int toInt(bool *ok) const const
Q_D(Todo)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Jan 24 2025 11:49:57 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.