Akonadi

trashrestorejob.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 "trashrestorejob.h"
8
9#include "entitydeletedattribute.h"
10#include "job_p.h"
11
12#include <KLocalizedString>
13
14#include "collectionfetchjob.h"
15#include "collectionfetchscope.h"
16#include "collectionmodifyjob.h"
17#include "collectionmovejob.h"
18#include "itemfetchjob.h"
19#include "itemfetchscope.h"
20#include "itemmodifyjob.h"
21#include "itemmovejob.h"
22
23#include "akonadicore_debug.h"
24
25#include <QHash>
26
27using namespace Akonadi;
28
29class Akonadi::TrashRestoreJobPrivate : public JobPrivate
30{
31public:
32 explicit TrashRestoreJobPrivate(TrashRestoreJob *parent)
33 : JobPrivate(parent)
34 {
35 }
36
37 void selectResult(KJob *job);
38
39 // Called when the target collection was fetched,
40 // will issue the move and the removal of the attributes if collection is valid
41 void targetCollectionFetched(KJob *job);
42
43 void removeAttribute(const Akonadi::Item::List &list);
44 void removeAttribute(const Akonadi::Collection::List &list);
45
46 // Called after initial fetch of items, issues fetch of target collection or removes attributes for in place restore
47 void itemsReceived(const Akonadi::Item::List &items);
48 void collectionsReceived(const Akonadi::Collection::List &collections);
49
50 Q_DECLARE_PUBLIC(TrashRestoreJob)
51
52 Item::List mItems;
53 Collection mCollection;
54 Collection mTargetCollection;
55 QHash<Collection, Item::List> restoreCollections; // groups items to target restore collections
56};
57
58void TrashRestoreJobPrivate::selectResult(KJob *job)
59{
60 Q_Q(TrashRestoreJob);
61 if (job->error()) {
62 qCWarning(AKONADICORE_LOG) << job->errorString();
63 return; // KCompositeJob takes care of errors
64 }
65
66 if (!q->hasSubjobs() || (q->subjobs().contains(static_cast<KJob *>(q->sender())) && q->subjobs().size() == 1)) {
67 // qCWarning(AKONADICORE_LOG) << "trash restore finished";
68 q->emitResult();
69 }
70}
71
72void TrashRestoreJobPrivate::targetCollectionFetched(KJob *job)
73{
74 Q_Q(TrashRestoreJob);
75
76 auto fetchJob = qobject_cast<CollectionFetchJob *>(job);
77 Q_ASSERT(fetchJob);
78 const Collection::List &list = fetchJob->collections();
79
80 if (list.isEmpty() || !list.first().isValid() || list.first().hasAttribute<Akonadi::EntityDeletedAttribute>()) { // target collection is invalid/not
81 // existing
82
83 const QString res = fetchJob->property("Resource").toString();
84 if (res.isEmpty()) { // There is no fallback
85 q->setError(Job::Unknown);
86 q->setErrorText(i18n("Could not find restore collection and restore resource is not available"));
87 q->emitResult();
88 // FAIL
89 qCWarning(AKONADICORE_LOG) << "restore collection not available";
90 return;
91 }
92
93 // Try again with the root collection of the resource as fallback
95 resRootFetch->fetchScope().setResource(res);
96 const QVariant &var = fetchJob->property("Items");
97 if (var.isValid()) {
98 resRootFetch->setProperty("Items", var.toInt());
99 }
100 q->connect(resRootFetch, &KJob::result, q, [this](KJob *job) {
101 targetCollectionFetched(job);
102 });
103 q->connect(resRootFetch, &KJob::result, q, [this](KJob *job) {
104 selectResult(job);
105 });
106 return;
107 }
108 Q_ASSERT(list.size() == 1);
109 // SUCCESS
110 // We know where to move the entity, so remove the attributes and move them to the right location
111 if (!mItems.isEmpty()) {
112 const QVariant &var = fetchJob->property("Items");
113 Q_ASSERT(var.isValid());
114 const Item::List &items = restoreCollections[Collection(var.toInt())];
115
116 // store removed attribute if destination collection is valid or the item doesn't have a restore collection
117 // TODO only remove the attribute if the move job was successful (although it is unlikely that it fails since we already fetched the collection)
118 removeAttribute(items);
119 if (items.first().parentCollection() != list.first()) {
120 auto job = new ItemMoveJob(items, list.first(), q);
121 q->connect(job, &KJob::result, q, [this](KJob *job) {
122 selectResult(job);
123 });
124 }
125 } else {
126 Q_ASSERT(mCollection.isValid());
127 // TODO only remove the attribute if the move job was successful
128 removeAttribute(Collection::List() << mCollection);
129 auto collectionFetchJob = new CollectionFetchJob(mCollection, CollectionFetchJob::Recursive, q);
130 q->connect(collectionFetchJob, &KJob::result, q, [this](KJob *job) {
131 selectResult(job);
132 });
133 q->connect(collectionFetchJob, &CollectionFetchJob::collectionsReceived, q, [this](const auto &cols) {
134 removeAttribute(cols);
135 });
136
137 if (mCollection.parentCollection() != list.first()) {
138 auto job = new CollectionMoveJob(mCollection, list.first(), q);
139 q->connect(job, &KJob::result, q, [this](KJob *job) {
140 selectResult(job);
141 });
142 }
143 }
144}
145
146void TrashRestoreJobPrivate::itemsReceived(const Akonadi::Item::List &items)
147{
148 Q_Q(TrashRestoreJob);
149 if (items.isEmpty()) {
150 q->setError(Job::Unknown);
151 q->setErrorText(i18n("Invalid items passed"));
152 q->emitResult();
153 return;
154 }
155 mItems = items;
156
157 // Sort by restore collection
158 for (const Item &item : std::as_const(mItems)) {
159 if (!item.hasAttribute<Akonadi::EntityDeletedAttribute>()) {
160 continue;
161 }
162 // If the restore collection is invalid we restore the item in place, so we don't need to know its restore resource => we can put those cases in the
163 // same list
164 restoreCollections[item.attribute<Akonadi::EntityDeletedAttribute>()->restoreCollection()].append(item);
165 }
166
167 for (auto it = restoreCollections.cbegin(), e = restoreCollections.cend(); it != e; ++it) {
168 const Item &first = it.value().first();
169 // Move the items to the correct collection if available
170 Collection targetCollection = it.key();
171 const QString restoreResource = first.attribute<Akonadi::EntityDeletedAttribute>()->restoreResource();
172
173 // Restore in place if no restore collection is set
174 if (!targetCollection.isValid()) {
175 removeAttribute(it.value());
176 return;
177 }
178
179 // Explicit target overrides the resource
180 if (mTargetCollection.isValid()) {
181 targetCollection = mTargetCollection;
182 }
183
184 // Try to fetch the target resource to see if it is available
185 auto fetchJob = new CollectionFetchJob(targetCollection, Akonadi::CollectionFetchJob::Base, q);
186 if (!mTargetCollection.isValid()) { // explicit targets don't have a fallback
187 fetchJob->setProperty("Resource", restoreResource);
188 }
189 fetchJob->setProperty("Items", it.key().id()); // to find the items in restore collections again
190 q->connect(fetchJob, &KJob::result, q, [this](KJob *job) {
191 targetCollectionFetched(job);
192 });
193 }
194}
195
196void TrashRestoreJobPrivate::collectionsReceived(const Akonadi::Collection::List &collections)
197{
198 Q_Q(TrashRestoreJob);
199 if (collections.isEmpty()) {
200 q->setError(Job::Unknown);
201 q->setErrorText(i18n("Invalid collection passed"));
202 q->emitResult();
203 return;
204 }
205 Q_ASSERT(collections.size() == 1);
206 mCollection = collections.first();
207
208 if (!mCollection.hasAttribute<Akonadi::EntityDeletedAttribute>()) {
209 return;
210 }
211
212 const QString restoreResource = mCollection.attribute<Akonadi::EntityDeletedAttribute>()->restoreResource();
213 Collection targetCollection = mCollection.attribute<EntityDeletedAttribute>()->restoreCollection();
214
215 // Restore in place if no restore collection/resource is set
216 if (!targetCollection.isValid()) {
217 removeAttribute(Collection::List() << mCollection);
218 auto collectionFetchJob = new CollectionFetchJob(mCollection, CollectionFetchJob::Recursive, q);
219 q->connect(collectionFetchJob, &KJob::result, q, [this](KJob *job) {
220 selectResult(job);
221 });
222 q->connect(collectionFetchJob, &CollectionFetchJob::collectionsReceived, q, [this](const auto &cols) {
223 removeAttribute(cols);
224 });
225 return;
226 }
227
228 // Explicit target overrides the resource/configured restore collection
229 if (mTargetCollection.isValid()) {
230 targetCollection = mTargetCollection;
231 }
232
233 // Fetch the target collection to check if it's valid
234 auto fetchJob = new CollectionFetchJob(targetCollection, CollectionFetchJob::Base, q);
235 if (!mTargetCollection.isValid()) { // explicit targets don't have a fallback
236 fetchJob->setProperty("Resource", restoreResource);
237 }
238 q->connect(fetchJob, &KJob::result, q, [this](KJob *job) {
239 targetCollectionFetched(job);
240 });
241}
242
243void TrashRestoreJobPrivate::removeAttribute(const Akonadi::Collection::List &list)
244{
245 Q_Q(TrashRestoreJob);
247 while (i.hasNext()) {
248 Collection col = i.next();
250
251 auto job = new CollectionModifyJob(col, q);
252 q->connect(job, &KJob::result, q, [this](KJob *job) {
253 selectResult(job);
254 });
255
256 auto itemFetchJob = new ItemFetchJob(col, q);
257 itemFetchJob->fetchScope().fetchAttribute<EntityDeletedAttribute>(true);
258 q->connect(itemFetchJob, &KJob::result, q, [this](KJob *job) {
259 selectResult(job);
260 });
261 q->connect(itemFetchJob, &ItemFetchJob::itemsReceived, q, [this](const auto &items) {
262 removeAttribute(items);
263 });
264 }
265}
266
267void TrashRestoreJobPrivate::removeAttribute(const Akonadi::Item::List &list)
268{
269 Q_Q(TrashRestoreJob);
270 Item::List items = list;
272 while (i.hasNext()) {
273 Item &item = i.next();
274 item.removeAttribute<EntityDeletedAttribute>();
275 auto job = new ItemModifyJob(item, q);
276 job->setIgnorePayload(true);
277 q->connect(job, &KJob::result, q, [this](KJob *job) {
278 selectResult(job);
279 });
280 }
281 // For some reason it is not possible to apply this change to multiple items at once
282 // ItemModifyJob *job = new ItemModifyJob(items, q);
283 // q->connect( job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)) );
284}
285
287 : Job(new TrashRestoreJobPrivate(this), parent)
288{
290 d->mItems << item;
291}
292
294 : Job(new TrashRestoreJobPrivate(this), parent)
295{
297 d->mItems = items;
298}
299
300TrashRestoreJob::TrashRestoreJob(const Collection &collection, QObject *parent)
301 : Job(new TrashRestoreJobPrivate(this), parent)
302{
304 d->mCollection = collection;
305}
306
307TrashRestoreJob::~TrashRestoreJob()
308{
309}
310
312{
314 d->mTargetCollection = collection;
315}
316
317Item::List TrashRestoreJob::items() const
318{
319 Q_D(const TrashRestoreJob);
320 return d->mItems;
321}
322
324{
326
327 // We always have to fetch the entities to ensure that the EntityDeletedAttribute is available
328 if (!d->mItems.isEmpty()) {
329 auto job = new ItemFetchJob(d->mItems, this);
330 job->fetchScope().setCacheOnly(true);
331 job->fetchScope().fetchAttribute<EntityDeletedAttribute>(true);
332 connect(job, &ItemFetchJob::itemsReceived, this, [d](const auto &items) {
333 d->itemsReceived(items);
334 });
335 } else if (d->mCollection.isValid()) {
336 auto job = new CollectionFetchJob(d->mCollection, CollectionFetchJob::Base, this);
337 connect(job, &CollectionFetchJob::collectionsReceived, this, [d](const auto &cols) {
338 d->collectionsReceived(cols);
339 });
340 } else {
341 qCWarning(AKONADICORE_LOG) << "No valid collection or empty itemlist";
343 setErrorText(i18n("No valid collection or empty itemlist"));
344 emitResult();
345 }
346}
347
348#include "moc_trashrestorejob.cpp"
Job that fetches collections from the Akonadi storage.
@ Recursive
List all sub-collections.
@ FirstLevel
Only list direct sub-collections of the base collection.
@ Base
Only fetch the base collection.
void collectionsReceived(const Akonadi::Collection::List &collections)
This signal is emitted whenever the job has received collections.
Job that modifies a collection in the Akonadi storage.
Job that moves a collection in the Akonadi storage to a new parent collection.
Represents a collection of PIM items.
Definition collection.h:62
bool hasAttribute(const QByteArray &name) const
Returns true if the collection has an attribute of the given type name, false otherwise.
static Collection root()
Returns the root collection.
Collection parentCollection() const
Returns the parent collection of this object.
void removeAttribute(const QByteArray &name)
Removes and deletes the attribute of the given type name.
Attribute * attribute(const QByteArray &name)
Returns the attribute of the given type name if available, 0 otherwise.
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.
Job that modifies an existing item in the Akonadi storage.
Job that moves an item into a different collection in the Akonadi storage.
Definition itemmovejob.h:35
Represents a PIM item stored in Akonadi storage.
Definition item.h:100
Collection parentCollection() const
Returns the parent collection of this object.
Definition item.cpp:161
Base class for all actions in the Akonadi storage.
Definition job.h:81
@ Unknown
Unknown error.
Definition job.h:102
Job that restores entities from trash.
TrashRestoreJob(const Item &item, QObject *parent=nullptr)
All items need to be from the same resource.
void doStart() override
This method must be reimplemented in the concrete jobs.
void setTargetCollection(const Collection &collection)
Sets the target collection, where the item is moved to.
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...)
Helper integration between Akonadi and Qt.
KIOCORE_EXPORT QStringList list(const QString &fileClass)
T & first()
bool isEmpty() const const
qsizetype size() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
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-2024 The KDE developers.
Generated on Fri Dec 6 2024 12:00:59 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.