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
94 auto resRootFetch = new CollectionFetchJob(Collection::root(), CollectionFetchJob::FirstLevel, q);
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);
246 QListIterator<Collection> i(list);
247 while (i.hasNext()) {
248 Collection col = i.next();
249 col.removeAttribute<EntityDeletedAttribute>();
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;
271 QMutableListIterator<Item> i(items);
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.
Represents a collection of PIM items.
Definition collection.h:62
static Collection root()
Returns the root collection.
QList< Collection > List
Describes a list of collections.
Definition collection.h:84
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.
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
QList< Item > List
Describes a list of items.
Definition item.h:110
Base class for all actions in the Akonadi storage.
Definition job.h:81
Job(QObject *parent=nullptr)
Creates a new job.
Definition job.cpp:290
@ 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)
QObject * parent() const const
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.