File indexing completed on 2024-11-10 04:40:32
0001 /* 0002 SPDX-FileCopyrightText: 2011 Christian Mollekopf <chrigi_1@fastmail.fm> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "trashjob.h" 0008 0009 #include "entitydeletedattribute.h" 0010 #include "job_p.h" 0011 #include "trashsettings.h" 0012 0013 #include <KLocalizedString> 0014 0015 #include "collectiondeletejob.h" 0016 #include "collectionfetchjob.h" 0017 #include "collectionfetchscope.h" 0018 #include "collectionmodifyjob.h" 0019 #include "collectionmovejob.h" 0020 #include "itemdeletejob.h" 0021 #include "itemfetchjob.h" 0022 #include "itemfetchscope.h" 0023 #include "itemmodifyjob.h" 0024 #include "itemmovejob.h" 0025 0026 #include "akonadicore_debug.h" 0027 0028 #include <QHash> 0029 0030 using namespace Akonadi; 0031 0032 class Akonadi::TrashJobPrivate : public JobPrivate 0033 { 0034 public: 0035 explicit TrashJobPrivate(TrashJob *parent) 0036 : JobPrivate(parent) 0037 { 0038 } 0039 // 4. 0040 void selectResult(KJob *job); 0041 // 3. 0042 // Helper functions to recursively set the attribute on deleted collections 0043 void setAttribute(const Akonadi::Collection::List & /*list*/); 0044 void setAttribute(const Akonadi::Item::List & /*list*/); 0045 // Set attributes after ensuring that move job was successful 0046 void setAttribute(KJob *job); 0047 0048 // 2. 0049 // called after parent of the trashed item was fetched (needed to see in which resource the item is in) 0050 void parentCollectionReceived(const Akonadi::Collection::List & /*collections*/); 0051 0052 // 1. 0053 // called after initial fetch of trashed items 0054 void itemsReceived(const Akonadi::Item::List & /*items*/); 0055 // called after initial fetch of trashed collection 0056 void collectionsReceived(const Akonadi::Collection::List & /*collections*/); 0057 0058 Q_DECLARE_PUBLIC(TrashJob) 0059 0060 Item::List mItems; 0061 Collection mCollection; 0062 Collection mRestoreCollection; 0063 Collection mTrashCollection; 0064 bool mKeepTrashInCollection = false; 0065 bool mSetRestoreCollection = false; // only set restore collection when moved to trash collection (not in place) 0066 bool mDeleteIfInTrash = false; 0067 QHash<Collection, Item::List> mCollectionItems; // list of trashed items sorted according to parent collection 0068 QHash<Item::Id, Collection> mParentCollections; // fetched parent collection of items (containing the resource name) 0069 }; 0070 0071 void TrashJobPrivate::selectResult(KJob *job) 0072 { 0073 Q_Q(TrashJob); 0074 if (job->error()) { 0075 qCWarning(AKONADICORE_LOG) << job->objectName(); 0076 qCWarning(AKONADICORE_LOG) << job->errorString(); 0077 return; // KCompositeJob takes care of errors 0078 } 0079 0080 if (!q->hasSubjobs() || (q->subjobs().contains(static_cast<KJob *>(q->sender())) && q->subjobs().size() == 1)) { 0081 q->emitResult(); 0082 } 0083 } 0084 0085 void TrashJobPrivate::setAttribute(const Akonadi::Collection::List &list) 0086 { 0087 Q_Q(TrashJob); 0088 QListIterator<Collection> i(list); 0089 while (i.hasNext()) { 0090 const Collection &col = i.next(); 0091 auto eda = new EntityDeletedAttribute(); 0092 if (mSetRestoreCollection) { 0093 Q_ASSERT(mRestoreCollection.isValid()); 0094 eda->setRestoreCollection(mRestoreCollection); 0095 } 0096 0097 Collection modCol(col.id()); // really only modify attribute (forget old remote ids, etc.), otherwise we have an error because of the move 0098 modCol.addAttribute(eda); 0099 0100 auto job = new CollectionModifyJob(modCol, q); 0101 q->connect(job, &KJob::result, q, [this](KJob *job) { 0102 selectResult(job); 0103 }); 0104 0105 auto itemFetchJob = new ItemFetchJob(col, q); 0106 // TODO not sure if it is guaranteed that itemsReceived is always before result (otherwise the result is emitted before the attributes are set) 0107 q->connect(itemFetchJob, &ItemFetchJob::itemsReceived, q, [this](const auto &items) { 0108 setAttribute(items); 0109 }); 0110 q->connect(itemFetchJob, &KJob::result, q, [this](KJob *job) { 0111 selectResult(job); 0112 }); 0113 } 0114 } 0115 0116 void TrashJobPrivate::setAttribute(const Akonadi::Item::List &list) 0117 { 0118 Q_Q(TrashJob); 0119 Item::List items = list; 0120 QMutableListIterator<Item> i(items); 0121 while (i.hasNext()) { 0122 const Item &item = i.next(); 0123 auto eda = new EntityDeletedAttribute(); 0124 if (mSetRestoreCollection) { 0125 // When deleting a collection, we want to restore the deleted collection's items restored to the deleted collection's parent, not the items parent 0126 if (mRestoreCollection.isValid()) { 0127 eda->setRestoreCollection(mRestoreCollection); 0128 } else { 0129 Q_ASSERT(mParentCollections.contains(item.parentCollection().id())); 0130 eda->setRestoreCollection(mParentCollections.value(item.parentCollection().id())); 0131 } 0132 } 0133 0134 Item modItem(item.id()); // really only modify attribute (forget old remote ids, etc.) 0135 modItem.addAttribute(eda); 0136 auto job = new ItemModifyJob(modItem, q); 0137 job->setIgnorePayload(true); 0138 q->connect(job, &KJob::result, q, [this](KJob *job) { 0139 selectResult(job); 0140 }); 0141 } 0142 0143 // For some reason it is not possible to apply this change to multiple items at once 0144 /*ItemModifyJob *job = new ItemModifyJob(items, q); 0145 q->connect( job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)) );*/ 0146 } 0147 0148 void TrashJobPrivate::setAttribute(KJob *job) 0149 { 0150 Q_Q(TrashJob); 0151 if (job->error()) { 0152 qCWarning(AKONADICORE_LOG) << job->objectName(); 0153 qCWarning(AKONADICORE_LOG) << job->errorString(); 0154 q->setError(Job::Unknown); 0155 q->setErrorText(i18n("Move to trash collection failed, aborting trash operation")); 0156 return; 0157 } 0158 0159 // For Items 0160 const QVariant var = job->property("MovedItems"); 0161 if (var.isValid()) { 0162 int id = var.toInt(); 0163 Q_ASSERT(id >= 0); 0164 setAttribute(mCollectionItems.value(Collection(id))); 0165 return; 0166 } 0167 0168 // For a collection 0169 Q_ASSERT(mCollection.isValid()); 0170 setAttribute(Collection::List() << mCollection); 0171 // Set the attribute on all subcollections and items 0172 auto colFetchJob = new CollectionFetchJob(mCollection, CollectionFetchJob::Recursive, q); 0173 q->connect(colFetchJob, &CollectionFetchJob::collectionsReceived, q, [this](const auto &cols) { 0174 setAttribute(cols); 0175 }); 0176 q->connect(colFetchJob, &KJob::result, q, [this](KJob *job) { 0177 selectResult(job); 0178 }); 0179 } 0180 0181 void TrashJobPrivate::parentCollectionReceived(const Akonadi::Collection::List &collections) 0182 { 0183 Q_Q(TrashJob); 0184 Q_ASSERT(collections.size() == 1); 0185 const Collection &parentCollection = collections.first(); 0186 0187 // store attribute 0188 Q_ASSERT(!parentCollection.resource().isEmpty()); 0189 Collection trashCollection = mTrashCollection; 0190 if (!mTrashCollection.isValid()) { 0191 trashCollection = TrashSettings::getTrashCollection(parentCollection.resource()); 0192 } 0193 if (!mKeepTrashInCollection && trashCollection.isValid()) { // Only set the restore collection if the item is moved to trash 0194 mSetRestoreCollection = true; 0195 } 0196 0197 mParentCollections.insert(parentCollection.id(), parentCollection); 0198 0199 if (trashCollection.isValid()) { // Move the items to the correct collection if available 0200 auto job = new ItemMoveJob(mCollectionItems.value(parentCollection), trashCollection, q); 0201 job->setProperty("MovedItems", parentCollection.id()); 0202 q->connect(job, &KJob::result, q, [this](KJob *job) { 0203 setAttribute(job); 0204 }); // Wait until the move finished to set the attribute 0205 q->connect(job, &KJob::result, q, [this](KJob *job) { 0206 selectResult(job); 0207 }); 0208 } else { 0209 setAttribute(mCollectionItems.value(parentCollection)); 0210 } 0211 } 0212 0213 void TrashJobPrivate::itemsReceived(const Akonadi::Item::List &items) 0214 { 0215 Q_Q(TrashJob); 0216 if (items.isEmpty()) { 0217 q->setError(Job::Unknown); 0218 q->setErrorText(i18n("Invalid items passed")); 0219 q->emitResult(); 0220 return; 0221 } 0222 0223 Item::List toDelete; 0224 QListIterator<Item> i(items); 0225 while (i.hasNext()) { 0226 const Item &item = i.next(); 0227 if (item.hasAttribute<EntityDeletedAttribute>()) { 0228 toDelete.append(item); 0229 continue; 0230 } 0231 Q_ASSERT(item.parentCollection().isValid()); 0232 mCollectionItems[item.parentCollection()].append(item); // Sort by parent col ( = restore collection) 0233 } 0234 0235 for (auto it = mCollectionItems.cbegin(), e = mCollectionItems.cend(); it != e; ++it) { 0236 auto job = new CollectionFetchJob(it.key(), Akonadi::CollectionFetchJob::Base, q); 0237 q->connect(job, &CollectionFetchJob::collectionsReceived, q, [this](const auto &cols) { 0238 parentCollectionReceived(cols); 0239 }); 0240 } 0241 0242 if (mDeleteIfInTrash && !toDelete.isEmpty()) { 0243 auto job = new ItemDeleteJob(toDelete, q); 0244 q->connect(job, &KJob::result, q, [this](KJob *job) { 0245 selectResult(job); 0246 }); 0247 } else if (mCollectionItems.isEmpty()) { // No job started, so we abort the job 0248 qCWarning(AKONADICORE_LOG) << "Nothing to do"; 0249 q->emitResult(); 0250 } 0251 } 0252 0253 void TrashJobPrivate::collectionsReceived(const Akonadi::Collection::List &collections) 0254 { 0255 Q_Q(TrashJob); 0256 if (collections.isEmpty()) { 0257 q->setError(Job::Unknown); 0258 q->setErrorText(i18n("Invalid collection passed")); 0259 q->emitResult(); 0260 return; 0261 } 0262 Q_ASSERT(collections.size() == 1); 0263 mCollection = collections.first(); 0264 0265 if (mCollection.hasAttribute<EntityDeletedAttribute>()) { // marked as deleted 0266 if (mDeleteIfInTrash) { 0267 auto job = new CollectionDeleteJob(mCollection, q); 0268 q->connect(job, &KJob::result, q, [this](KJob *job) { 0269 selectResult(job); 0270 }); 0271 } else { 0272 qCWarning(AKONADICORE_LOG) << "Nothing to do"; 0273 q->emitResult(); 0274 } 0275 return; 0276 } 0277 0278 Collection trashCollection = mTrashCollection; 0279 if (!mTrashCollection.isValid()) { 0280 trashCollection = TrashSettings::getTrashCollection(mCollection.resource()); 0281 } 0282 if (!mKeepTrashInCollection && trashCollection.isValid()) { // only set the restore collection if the item is moved to trash 0283 mSetRestoreCollection = true; 0284 Q_ASSERT(mCollection.parentCollection().isValid()); 0285 mRestoreCollection = mCollection.parentCollection(); 0286 mRestoreCollection.setResource(mCollection.resource()); // The parent collection doesn't contain the resource, so we have to set it manually 0287 } 0288 0289 if (trashCollection.isValid()) { 0290 auto job = new CollectionMoveJob(mCollection, trashCollection, q); 0291 q->connect(job, &KJob::result, q, [this](KJob *job) { 0292 setAttribute(job); 0293 }); 0294 q->connect(job, &KJob::result, q, [this](KJob *job) { 0295 selectResult(job); 0296 }); 0297 } else { 0298 setAttribute(Collection::List() << mCollection); 0299 } 0300 } 0301 0302 TrashJob::TrashJob(const Item &item, QObject *parent) 0303 : Job(new TrashJobPrivate(this), parent) 0304 { 0305 Q_D(TrashJob); 0306 d->mItems << item; 0307 } 0308 0309 TrashJob::TrashJob(const Item::List &items, QObject *parent) 0310 : Job(new TrashJobPrivate(this), parent) 0311 { 0312 Q_D(TrashJob); 0313 d->mItems = items; 0314 } 0315 0316 TrashJob::TrashJob(const Collection &collection, QObject *parent) 0317 : Job(new TrashJobPrivate(this), parent) 0318 { 0319 Q_D(TrashJob); 0320 d->mCollection = collection; 0321 } 0322 0323 TrashJob::~TrashJob() 0324 { 0325 } 0326 0327 Item::List TrashJob::items() const 0328 { 0329 Q_D(const TrashJob); 0330 return d->mItems; 0331 } 0332 0333 void TrashJob::setTrashCollection(const Akonadi::Collection &collection) 0334 { 0335 Q_D(TrashJob); 0336 d->mTrashCollection = collection; 0337 } 0338 0339 void TrashJob::keepTrashInCollection(bool enable) 0340 { 0341 Q_D(TrashJob); 0342 d->mKeepTrashInCollection = enable; 0343 } 0344 0345 void TrashJob::deleteIfInTrash(bool enable) 0346 { 0347 Q_D(TrashJob); 0348 d->mDeleteIfInTrash = enable; 0349 } 0350 0351 void TrashJob::doStart() 0352 { 0353 Q_D(TrashJob); 0354 0355 // Fetch items first to ensure that the EntityDeletedAttribute is available 0356 if (!d->mItems.isEmpty()) { 0357 auto job = new ItemFetchJob(d->mItems, this); 0358 job->fetchScope().setAncestorRetrieval(Akonadi::ItemFetchScope::Parent); // so we have access to the resource 0359 // job->fetchScope().setCacheOnly(true); 0360 job->fetchScope().fetchAttribute<EntityDeletedAttribute>(true); 0361 connect(job, &ItemFetchJob::itemsReceived, this, [d](const auto &items) { 0362 d->itemsReceived(items); 0363 }); 0364 0365 } else if (d->mCollection.isValid()) { 0366 auto job = new CollectionFetchJob(d->mCollection, CollectionFetchJob::Base, this); 0367 job->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::Parent); 0368 connect(job, &CollectionFetchJob::collectionsReceived, this, [d](const auto &cols) { 0369 d->collectionsReceived(cols); 0370 }); 0371 0372 } else { 0373 qCWarning(AKONADICORE_LOG) << "No valid collection or empty itemlist"; 0374 setError(Job::Unknown); 0375 setErrorText(i18n("No valid collection or empty itemlist")); 0376 emitResult(); 0377 } 0378 } 0379 0380 #include "moc_trashjob.cpp"