File indexing completed on 2024-11-10 04:40:33

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 "trashrestorejob.h"
0008 
0009 #include "entitydeletedattribute.h"
0010 #include "job_p.h"
0011 
0012 #include <KLocalizedString>
0013 
0014 #include "collectionfetchjob.h"
0015 #include "collectionfetchscope.h"
0016 #include "collectionmodifyjob.h"
0017 #include "collectionmovejob.h"
0018 #include "itemfetchjob.h"
0019 #include "itemfetchscope.h"
0020 #include "itemmodifyjob.h"
0021 #include "itemmovejob.h"
0022 
0023 #include "akonadicore_debug.h"
0024 
0025 #include <QHash>
0026 
0027 using namespace Akonadi;
0028 
0029 class Akonadi::TrashRestoreJobPrivate : public JobPrivate
0030 {
0031 public:
0032     explicit TrashRestoreJobPrivate(TrashRestoreJob *parent)
0033         : JobPrivate(parent)
0034     {
0035     }
0036 
0037     void selectResult(KJob *job);
0038 
0039     // Called when the target collection was fetched,
0040     // will issue the move and the removal of the attributes if collection is valid
0041     void targetCollectionFetched(KJob *job);
0042 
0043     void removeAttribute(const Akonadi::Item::List &list);
0044     void removeAttribute(const Akonadi::Collection::List &list);
0045 
0046     // Called after initial fetch of items, issues fetch of target collection or removes attributes for in place restore
0047     void itemsReceived(const Akonadi::Item::List &items);
0048     void collectionsReceived(const Akonadi::Collection::List &collections);
0049 
0050     Q_DECLARE_PUBLIC(TrashRestoreJob)
0051 
0052     Item::List mItems;
0053     Collection mCollection;
0054     Collection mTargetCollection;
0055     QHash<Collection, Item::List> restoreCollections; // groups items to target restore collections
0056 };
0057 
0058 void TrashRestoreJobPrivate::selectResult(KJob *job)
0059 {
0060     Q_Q(TrashRestoreJob);
0061     if (job->error()) {
0062         qCWarning(AKONADICORE_LOG) << job->errorString();
0063         return; // KCompositeJob takes care of errors
0064     }
0065 
0066     if (!q->hasSubjobs() || (q->subjobs().contains(static_cast<KJob *>(q->sender())) && q->subjobs().size() == 1)) {
0067         // qCWarning(AKONADICORE_LOG) << "trash restore finished";
0068         q->emitResult();
0069     }
0070 }
0071 
0072 void TrashRestoreJobPrivate::targetCollectionFetched(KJob *job)
0073 {
0074     Q_Q(TrashRestoreJob);
0075 
0076     auto fetchJob = qobject_cast<CollectionFetchJob *>(job);
0077     Q_ASSERT(fetchJob);
0078     const Collection::List &list = fetchJob->collections();
0079 
0080     if (list.isEmpty() || !list.first().isValid() || list.first().hasAttribute<Akonadi::EntityDeletedAttribute>()) { // target collection is invalid/not
0081                                                                                                                      // existing
0082 
0083         const QString res = fetchJob->property("Resource").toString();
0084         if (res.isEmpty()) { // There is no fallback
0085             q->setError(Job::Unknown);
0086             q->setErrorText(i18n("Could not find restore collection and restore resource is not available"));
0087             q->emitResult();
0088             // FAIL
0089             qCWarning(AKONADICORE_LOG) << "restore collection not available";
0090             return;
0091         }
0092 
0093         // Try again with the root collection of the resource as fallback
0094         auto resRootFetch = new CollectionFetchJob(Collection::root(), CollectionFetchJob::FirstLevel, q);
0095         resRootFetch->fetchScope().setResource(res);
0096         const QVariant &var = fetchJob->property("Items");
0097         if (var.isValid()) {
0098             resRootFetch->setProperty("Items", var.toInt());
0099         }
0100         q->connect(resRootFetch, &KJob::result, q, [this](KJob *job) {
0101             targetCollectionFetched(job);
0102         });
0103         q->connect(resRootFetch, &KJob::result, q, [this](KJob *job) {
0104             selectResult(job);
0105         });
0106         return;
0107     }
0108     Q_ASSERT(list.size() == 1);
0109     // SUCCESS
0110     // We know where to move the entity, so remove the attributes and move them to the right location
0111     if (!mItems.isEmpty()) {
0112         const QVariant &var = fetchJob->property("Items");
0113         Q_ASSERT(var.isValid());
0114         const Item::List &items = restoreCollections[Collection(var.toInt())];
0115 
0116         // store removed attribute if destination collection is valid or the item doesn't have a restore collection
0117         // TODO only remove the attribute if the move job was successful (although it is unlikely that it fails since we already fetched the collection)
0118         removeAttribute(items);
0119         if (items.first().parentCollection() != list.first()) {
0120             auto job = new ItemMoveJob(items, list.first(), q);
0121             q->connect(job, &KJob::result, q, [this](KJob *job) {
0122                 selectResult(job);
0123             });
0124         }
0125     } else {
0126         Q_ASSERT(mCollection.isValid());
0127         // TODO only remove the attribute if the move job was successful
0128         removeAttribute(Collection::List() << mCollection);
0129         auto collectionFetchJob = new CollectionFetchJob(mCollection, CollectionFetchJob::Recursive, q);
0130         q->connect(collectionFetchJob, &KJob::result, q, [this](KJob *job) {
0131             selectResult(job);
0132         });
0133         q->connect(collectionFetchJob, &CollectionFetchJob::collectionsReceived, q, [this](const auto &cols) {
0134             removeAttribute(cols);
0135         });
0136 
0137         if (mCollection.parentCollection() != list.first()) {
0138             auto job = new CollectionMoveJob(mCollection, list.first(), q);
0139             q->connect(job, &KJob::result, q, [this](KJob *job) {
0140                 selectResult(job);
0141             });
0142         }
0143     }
0144 }
0145 
0146 void TrashRestoreJobPrivate::itemsReceived(const Akonadi::Item::List &items)
0147 {
0148     Q_Q(TrashRestoreJob);
0149     if (items.isEmpty()) {
0150         q->setError(Job::Unknown);
0151         q->setErrorText(i18n("Invalid items passed"));
0152         q->emitResult();
0153         return;
0154     }
0155     mItems = items;
0156 
0157     // Sort by restore collection
0158     for (const Item &item : std::as_const(mItems)) {
0159         if (!item.hasAttribute<Akonadi::EntityDeletedAttribute>()) {
0160             continue;
0161         }
0162         // 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
0163         // same list
0164         restoreCollections[item.attribute<Akonadi::EntityDeletedAttribute>()->restoreCollection()].append(item);
0165     }
0166 
0167     for (auto it = restoreCollections.cbegin(), e = restoreCollections.cend(); it != e; ++it) {
0168         const Item &first = it.value().first();
0169         // Move the items to the correct collection if available
0170         Collection targetCollection = it.key();
0171         const QString restoreResource = first.attribute<Akonadi::EntityDeletedAttribute>()->restoreResource();
0172 
0173         // Restore in place if no restore collection is set
0174         if (!targetCollection.isValid()) {
0175             removeAttribute(it.value());
0176             return;
0177         }
0178 
0179         // Explicit target overrides the resource
0180         if (mTargetCollection.isValid()) {
0181             targetCollection = mTargetCollection;
0182         }
0183 
0184         // Try to fetch the target resource to see if it is available
0185         auto fetchJob = new CollectionFetchJob(targetCollection, Akonadi::CollectionFetchJob::Base, q);
0186         if (!mTargetCollection.isValid()) { // explicit targets don't have a fallback
0187             fetchJob->setProperty("Resource", restoreResource);
0188         }
0189         fetchJob->setProperty("Items", it.key().id()); // to find the items in restore collections again
0190         q->connect(fetchJob, &KJob::result, q, [this](KJob *job) {
0191             targetCollectionFetched(job);
0192         });
0193     }
0194 }
0195 
0196 void TrashRestoreJobPrivate::collectionsReceived(const Akonadi::Collection::List &collections)
0197 {
0198     Q_Q(TrashRestoreJob);
0199     if (collections.isEmpty()) {
0200         q->setError(Job::Unknown);
0201         q->setErrorText(i18n("Invalid collection passed"));
0202         q->emitResult();
0203         return;
0204     }
0205     Q_ASSERT(collections.size() == 1);
0206     mCollection = collections.first();
0207 
0208     if (!mCollection.hasAttribute<Akonadi::EntityDeletedAttribute>()) {
0209         return;
0210     }
0211 
0212     const QString restoreResource = mCollection.attribute<Akonadi::EntityDeletedAttribute>()->restoreResource();
0213     Collection targetCollection = mCollection.attribute<EntityDeletedAttribute>()->restoreCollection();
0214 
0215     // Restore in place if no restore collection/resource is set
0216     if (!targetCollection.isValid()) {
0217         removeAttribute(Collection::List() << mCollection);
0218         auto collectionFetchJob = new CollectionFetchJob(mCollection, CollectionFetchJob::Recursive, q);
0219         q->connect(collectionFetchJob, &KJob::result, q, [this](KJob *job) {
0220             selectResult(job);
0221         });
0222         q->connect(collectionFetchJob, &CollectionFetchJob::collectionsReceived, q, [this](const auto &cols) {
0223             removeAttribute(cols);
0224         });
0225         return;
0226     }
0227 
0228     // Explicit target overrides the resource/configured restore collection
0229     if (mTargetCollection.isValid()) {
0230         targetCollection = mTargetCollection;
0231     }
0232 
0233     // Fetch the target collection to check if it's valid
0234     auto fetchJob = new CollectionFetchJob(targetCollection, CollectionFetchJob::Base, q);
0235     if (!mTargetCollection.isValid()) { // explicit targets don't have a fallback
0236         fetchJob->setProperty("Resource", restoreResource);
0237     }
0238     q->connect(fetchJob, &KJob::result, q, [this](KJob *job) {
0239         targetCollectionFetched(job);
0240     });
0241 }
0242 
0243 void TrashRestoreJobPrivate::removeAttribute(const Akonadi::Collection::List &list)
0244 {
0245     Q_Q(TrashRestoreJob);
0246     QListIterator<Collection> i(list);
0247     while (i.hasNext()) {
0248         Collection col = i.next();
0249         col.removeAttribute<EntityDeletedAttribute>();
0250 
0251         auto job = new CollectionModifyJob(col, q);
0252         q->connect(job, &KJob::result, q, [this](KJob *job) {
0253             selectResult(job);
0254         });
0255 
0256         auto itemFetchJob = new ItemFetchJob(col, q);
0257         itemFetchJob->fetchScope().fetchAttribute<EntityDeletedAttribute>(true);
0258         q->connect(itemFetchJob, &KJob::result, q, [this](KJob *job) {
0259             selectResult(job);
0260         });
0261         q->connect(itemFetchJob, &ItemFetchJob::itemsReceived, q, [this](const auto &items) {
0262             removeAttribute(items);
0263         });
0264     }
0265 }
0266 
0267 void TrashRestoreJobPrivate::removeAttribute(const Akonadi::Item::List &list)
0268 {
0269     Q_Q(TrashRestoreJob);
0270     Item::List items = list;
0271     QMutableListIterator<Item> i(items);
0272     while (i.hasNext()) {
0273         Item &item = i.next();
0274         item.removeAttribute<EntityDeletedAttribute>();
0275         auto job = new ItemModifyJob(item, q);
0276         job->setIgnorePayload(true);
0277         q->connect(job, &KJob::result, q, [this](KJob *job) {
0278             selectResult(job);
0279         });
0280     }
0281     // For some reason it is not possible to apply this change to multiple items at once
0282     // ItemModifyJob *job = new ItemModifyJob(items, q);
0283     // q->connect( job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)) );
0284 }
0285 
0286 TrashRestoreJob::TrashRestoreJob(const Item &item, QObject *parent)
0287     : Job(new TrashRestoreJobPrivate(this), parent)
0288 {
0289     Q_D(TrashRestoreJob);
0290     d->mItems << item;
0291 }
0292 
0293 TrashRestoreJob::TrashRestoreJob(const Item::List &items, QObject *parent)
0294     : Job(new TrashRestoreJobPrivate(this), parent)
0295 {
0296     Q_D(TrashRestoreJob);
0297     d->mItems = items;
0298 }
0299 
0300 TrashRestoreJob::TrashRestoreJob(const Collection &collection, QObject *parent)
0301     : Job(new TrashRestoreJobPrivate(this), parent)
0302 {
0303     Q_D(TrashRestoreJob);
0304     d->mCollection = collection;
0305 }
0306 
0307 TrashRestoreJob::~TrashRestoreJob()
0308 {
0309 }
0310 
0311 void TrashRestoreJob::setTargetCollection(const Akonadi::Collection &collection)
0312 {
0313     Q_D(TrashRestoreJob);
0314     d->mTargetCollection = collection;
0315 }
0316 
0317 Item::List TrashRestoreJob::items() const
0318 {
0319     Q_D(const TrashRestoreJob);
0320     return d->mItems;
0321 }
0322 
0323 void TrashRestoreJob::doStart()
0324 {
0325     Q_D(TrashRestoreJob);
0326 
0327     // We always have to fetch the entities to ensure that the EntityDeletedAttribute is available
0328     if (!d->mItems.isEmpty()) {
0329         auto job = new ItemFetchJob(d->mItems, this);
0330         job->fetchScope().setCacheOnly(true);
0331         job->fetchScope().fetchAttribute<EntityDeletedAttribute>(true);
0332         connect(job, &ItemFetchJob::itemsReceived, this, [d](const auto &items) {
0333             d->itemsReceived(items);
0334         });
0335     } else if (d->mCollection.isValid()) {
0336         auto job = new CollectionFetchJob(d->mCollection, CollectionFetchJob::Base, this);
0337         connect(job, &CollectionFetchJob::collectionsReceived, this, [d](const auto &cols) {
0338             d->collectionsReceived(cols);
0339         });
0340     } else {
0341         qCWarning(AKONADICORE_LOG) << "No valid collection or empty itemlist";
0342         setError(Job::Unknown);
0343         setErrorText(i18n("No valid collection or empty itemlist"));
0344         emitResult();
0345     }
0346 }
0347 
0348 #include "moc_trashrestorejob.cpp"