File indexing completed on 2024-06-16 04:50:12

0001 /*
0002     SPDX-FileCopyrightText: 2007, 2009 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "akonadicore_debug.h"
0008 #include "collection.h"
0009 #include "collectioncreatejob.h"
0010 #include "collectiondeletejob.h"
0011 #include "collectionfetchjob.h"
0012 #include "collectionfetchscope.h"
0013 #include "collectionmodifyjob.h"
0014 #include "collectionmovejob.h"
0015 #include "collectionsync_p.h"
0016 
0017 #include "cachepolicy.h"
0018 
0019 #include <KLocalizedString>
0020 #include <QHash>
0021 #include <QList>
0022 
0023 #include <functional>
0024 
0025 using namespace Akonadi;
0026 
0027 static const char CONTENTMIMETYPES[] = "CONTENTMIMETYPES";
0028 
0029 static const char ROOTPARENTRID[] = "AKONADI_ROOT_COLLECTION";
0030 
0031 class RemoteId
0032 {
0033 public:
0034     explicit RemoteId()
0035     {
0036     }
0037 
0038     explicit inline RemoteId(const QStringList &ridChain)
0039         : ridChain(ridChain)
0040     {
0041     }
0042 
0043     explicit inline RemoteId(const QString &rid)
0044     {
0045         ridChain.append(rid);
0046     }
0047 
0048     inline bool isAbsolute() const
0049     {
0050         return ridChain.last() == QString::fromLatin1(ROOTPARENTRID);
0051     }
0052 
0053     inline bool isEmpty() const
0054     {
0055         return ridChain.isEmpty();
0056     }
0057 
0058     inline bool operator==(const RemoteId &other) const
0059     {
0060         return ridChain == other.ridChain;
0061     }
0062 
0063     QStringList ridChain;
0064 
0065     static RemoteId rootRid;
0066 };
0067 
0068 RemoteId RemoteId::rootRid = RemoteId(QStringList() << QString::fromLatin1(ROOTPARENTRID));
0069 
0070 Q_DECLARE_METATYPE(RemoteId)
0071 
0072 size_t qHash(const RemoteId &rid, size_t seed = 0) noexcept
0073 {
0074     return qHashRange(rid.ridChain.constBegin(), rid.ridChain.constEnd(), seed);
0075 }
0076 
0077 inline bool operator<(const RemoteId &r1, const RemoteId &r2)
0078 {
0079     if (r1.ridChain.length() == r2.ridChain.length()) {
0080         auto it1 = r1.ridChain.constBegin();
0081         auto end1 = r1.ridChain.constEnd();
0082         auto it2 = r2.ridChain.constBegin();
0083         while (it1 != end1) {
0084             if ((*it1) == (*it2)) {
0085                 ++it1;
0086                 ++it2;
0087                 continue;
0088             }
0089             return (*it1) < (*it2);
0090         }
0091     } else {
0092         return r1.ridChain.length() < r2.ridChain.length();
0093     }
0094     return false;
0095 }
0096 
0097 QDebug operator<<(QDebug s, const RemoteId &rid)
0098 {
0099     s.nospace() << "RemoteId(" << rid.ridChain << ")";
0100     return s;
0101 }
0102 
0103 /**
0104  * @internal
0105  */
0106 class Akonadi::CollectionSyncPrivate
0107 {
0108 public:
0109     explicit CollectionSyncPrivate(CollectionSync *parent)
0110         : q(parent)
0111         , pendingJobs(0)
0112         , progress(0)
0113         , currentTransaction(nullptr)
0114         , incremental(false)
0115         , streaming(false)
0116         , hierarchicalRIDs(false)
0117         , localListDone(false)
0118         , deliveryDone(false)
0119         , akonadiRootCollection(Collection::root())
0120         , resultEmitted(false)
0121     {
0122     }
0123 
0124     ~CollectionSyncPrivate()
0125     {
0126     }
0127 
0128     RemoteId remoteIdForCollection(const Collection &collection) const
0129     {
0130         if (collection == Collection::root()) {
0131             return RemoteId::rootRid;
0132         }
0133 
0134         if (!hierarchicalRIDs) {
0135             return RemoteId(collection.remoteId());
0136         }
0137 
0138         RemoteId rid;
0139         Collection parent = collection;
0140         while (parent.isValid() || !parent.remoteId().isEmpty()) {
0141             QString prid = parent.remoteId();
0142             if (prid.isEmpty() && parent.isValid()) {
0143                 prid = uidRidMap.value(parent.id());
0144             }
0145             if (prid.isEmpty()) {
0146                 break;
0147             }
0148             rid.ridChain.append(prid);
0149             parent = parent.parentCollection();
0150             if (parent == akonadiRootCollection) {
0151                 rid.ridChain.append(QString::fromLatin1(ROOTPARENTRID));
0152                 break;
0153             }
0154         }
0155         return rid;
0156     }
0157 
0158     void addRemoteColection(const Collection &collection, bool removed = false)
0159     {
0160         QHash<RemoteId, Collection::List> &map = (removed ? removedRemoteCollections : remoteCollections);
0161         const Collection parentCollection = collection.parentCollection();
0162         if (parentCollection.remoteId() == akonadiRootCollection.remoteId() || parentCollection.id() == akonadiRootCollection.id()) {
0163             Collection c2(collection);
0164             c2.setParentCollection(akonadiRootCollection);
0165             map[RemoteId::rootRid].append(c2);
0166         } else {
0167             Q_ASSERT(!parentCollection.remoteId().isEmpty());
0168             map[remoteIdForCollection(parentCollection)].append(collection);
0169         }
0170     }
0171 
0172     /* Compares collections by remoteId and falls back to name comparison in case
0173      * local collection does not have remoteId (which can happen in some cases)
0174      */
0175     bool matchLocalAndRemoteCollection(const Collection &local, const Collection &remote)
0176     {
0177         if (!local.remoteId().isEmpty()) {
0178             return local.remoteId() == remote.remoteId();
0179         } else {
0180             return local.name() == remote.name();
0181         }
0182     }
0183 
0184     void localCollectionsReceived(const Akonadi::Collection::List &localCols)
0185     {
0186         for (const Akonadi::Collection &collection : localCols) {
0187             const RemoteId parentRid = remoteIdForCollection(collection.parentCollection());
0188             localCollections[parentRid] += collection;
0189         }
0190     }
0191 
0192     void processCollections(const RemoteId &parentRid)
0193     {
0194         Collection::List remoteChildren = remoteCollections.value(parentRid);
0195         Collection::List removedChildren = removedRemoteCollections.value(parentRid);
0196         Collection::List localChildren = localCollections.value(parentRid);
0197 
0198         // Iterate over the list of local children of localParent
0199         for (auto localIter = localChildren.begin(), localEnd = localChildren.end(); localIter != localEnd;) {
0200             const Collection localCollection = *localIter;
0201             bool matched = false;
0202             uidRidMap.insert(localIter->id(), localIter->remoteId());
0203 
0204             // Try to map removed remote collections (from incremental sync) to local collections
0205             for (auto removedIter = removedChildren.begin(), removedEnd = removedChildren.end(); removedIter != removedEnd;) {
0206                 Collection removedCollection = *removedIter;
0207 
0208                 if (matchLocalAndRemoteCollection(localCollection, removedCollection)) {
0209                     matched = true;
0210                     if (!localCollection.remoteId().isEmpty()) {
0211                         localCollectionsToRemove.append(localCollection);
0212                     }
0213                     // Remove the matched removed collection from the list so that
0214                     // we don't have to iterate over it again next time.
0215                     removedIter = removedChildren.erase(removedIter);
0216                     removedEnd = removedChildren.end();
0217                     break;
0218                 } else {
0219                     // Keep looking
0220                     ++removedIter;
0221                 }
0222             }
0223 
0224             if (matched) {
0225                 // Remove the matched local collection from the list, because we
0226                 // have already put it into localCollectionsToRemove
0227                 localIter = localChildren.erase(localIter);
0228                 localEnd = localChildren.end();
0229                 continue;
0230             }
0231 
0232             // Try to find a matching collection in the list of remote children
0233             for (auto remoteIter = remoteChildren.begin(), remoteEnd = remoteChildren.end(); !matched && remoteIter != remoteEnd;) {
0234                 Collection remoteCollection = *remoteIter;
0235 
0236                 // Yay, we found a match!
0237                 if (matchLocalAndRemoteCollection(localCollection, remoteCollection)) {
0238                     matched = true;
0239 
0240                     // "Virtual" flag cannot be updated: we need to recreate
0241                     // the collection from scratch.
0242                     if (localCollection.isVirtual() != remoteCollection.isVirtual()) {
0243                         // Mark the local collection and all its children for deletion and re-creation
0244                         QList<QPair<Collection /*local*/, Collection /*remote*/>> parents = {{localCollection, remoteCollection}};
0245                         while (!parents.empty()) {
0246                             auto parent = parents.takeFirst();
0247                             qCDebug(AKONADICORE_LOG) << "Local collection " << parent.first.name() << " will be recreated";
0248                             localCollectionsToRemove.push_back(parent.first);
0249                             remoteCollectionsToCreate.push_back(parent.second);
0250                             for (auto it = localChildren.begin(), end = localChildren.end(); it != end;) {
0251                                 if (it->parentCollection() == parent.first) {
0252                                     Collection remoteParent;
0253                                     auto remoteIt = std::find_if(
0254                                         remoteChildren.begin(),
0255                                         remoteChildren.end(),
0256                                         std::bind(&CollectionSyncPrivate::matchLocalAndRemoteCollection, this, parent.first, std::placeholders::_1));
0257                                     if (remoteIt != remoteChildren.end()) {
0258                                         remoteParent = *remoteIt;
0259                                         remoteEnd = remoteChildren.erase(remoteIt);
0260                                     }
0261                                     parents.push_back({*it, remoteParent});
0262                                     it = localChildren.erase(it);
0263                                     localEnd = end = localChildren.end();
0264                                 } else {
0265                                     ++it;
0266                                 }
0267                             }
0268                         }
0269                     } else if (collectionNeedsUpdate(localCollection, remoteCollection)) {
0270                         // We need to store both local and remote collections, so that
0271                         // we can copy over attributes to be preserved
0272                         remoteCollectionsToUpdate.append(qMakePair(localCollection, remoteCollection));
0273                     } else {
0274                         // Collections are the same, no need to update anything
0275                     }
0276 
0277                     // Remove the matched remote collection from the list so that
0278                     // in the end we are left with list of collections that don't
0279                     // exist locally (i.e. new collections)
0280                     remoteIter = remoteChildren.erase(remoteIter);
0281                     remoteEnd = remoteChildren.end();
0282                     break;
0283                 } else {
0284                     // Keep looking
0285                     ++remoteIter;
0286                 }
0287             }
0288 
0289             if (matched) {
0290                 // Remove the matched local collection from the list so that
0291                 // in the end we are left with list of collections that don't
0292                 // exist remotely (i.e. removed collections)
0293                 localIter = localChildren.erase(localIter);
0294                 localEnd = localChildren.end();
0295             } else {
0296                 ++localIter;
0297             }
0298         }
0299 
0300         if (!removedChildren.isEmpty()) {
0301             removedRemoteCollections[parentRid] = removedChildren;
0302         } else {
0303             removedRemoteCollections.remove(parentRid);
0304         }
0305 
0306         if (!remoteChildren.isEmpty()) {
0307             remoteCollections[parentRid] = remoteChildren;
0308         } else {
0309             remoteCollections.remove(parentRid);
0310         }
0311 
0312         if (!localChildren.isEmpty()) {
0313             localCollections[parentRid] = localChildren;
0314         } else {
0315             localCollections.remove(parentRid);
0316         }
0317     }
0318 
0319     void processLocalCollections(const RemoteId &parentRid, const Collection &parentCollection)
0320     {
0321         const Collection::List originalChildren = localCollections.value(parentRid);
0322         processCollections(parentRid);
0323 
0324         const Collection::List remoteChildren = remoteCollections.take(parentRid);
0325         const Collection::List localChildren = localCollections.take(parentRid);
0326 
0327         // At this point remoteChildren contains collections that don't exist locally yet
0328         if (!remoteChildren.isEmpty()) {
0329             for (Collection c : remoteChildren) {
0330                 c.setParentCollection(parentCollection);
0331                 remoteCollectionsToCreate.append(c);
0332             }
0333         }
0334         // At this point localChildren contains collections that don't exist remotely anymore
0335         if (!localChildren.isEmpty() && !incremental) {
0336             for (const auto &c : localChildren) {
0337                 if (!c.remoteId().isEmpty()) {
0338                     localCollectionsToRemove.push_back(c);
0339                 }
0340             }
0341         }
0342 
0343         // Recurse into children
0344         for (const Collection &c : originalChildren) {
0345             processLocalCollections(remoteIdForCollection(c), c);
0346         }
0347     }
0348 
0349     void localCollectionFetchResult(KJob *job)
0350     {
0351         if (job->error()) {
0352             return; // handled by the base class
0353         }
0354 
0355         processLocalCollections(RemoteId::rootRid, akonadiRootCollection);
0356         localListDone = true;
0357         execute();
0358     }
0359 
0360     bool ignoreAttributeChanges(const Akonadi::Collection &col, const QByteArray &attribute) const
0361     {
0362         return (keepLocalChanges.contains(attribute) || col.keepLocalChanges().contains(attribute));
0363     }
0364 
0365     /**
0366       Checks if the given localCollection and remoteCollection are different
0367     */
0368     bool collectionNeedsUpdate(const Collection &localCollection, const Collection &remoteCollection) const
0369     {
0370         if (!ignoreAttributeChanges(remoteCollection, CONTENTMIMETYPES)) {
0371             if (localCollection.contentMimeTypes().size() != remoteCollection.contentMimeTypes().size()) {
0372                 return true;
0373             } else {
0374                 for (qsizetype i = 0, total = remoteCollection.contentMimeTypes().size(); i < total; ++i) {
0375                     const auto contentMimeTypes = remoteCollection.contentMimeTypes();
0376                     const QString mimeType = contentMimeTypes.at(i);
0377                     if (!localCollection.contentMimeTypes().contains(mimeType)) {
0378                         return true;
0379                     }
0380                 }
0381             }
0382         }
0383 
0384         if (localCollection.parentCollection().remoteId() != remoteCollection.parentCollection().remoteId()) {
0385             return true;
0386         }
0387         if (localCollection.name() != remoteCollection.name()) {
0388             return true;
0389         }
0390         if (localCollection.remoteId() != remoteCollection.remoteId()) {
0391             return true;
0392         }
0393         if (localCollection.remoteRevision() != remoteCollection.remoteRevision()) {
0394             return true;
0395         }
0396         if (!(localCollection.cachePolicy() == remoteCollection.cachePolicy())) {
0397             return true;
0398         }
0399         if (localCollection.enabled() != remoteCollection.enabled()) {
0400             return true;
0401         }
0402 
0403         // CollectionModifyJob adds the remote attributes to the local collection
0404         const Akonadi::Attribute::List lstAttr = remoteCollection.attributes();
0405         for (const Attribute *attr : lstAttr) {
0406             const Attribute *localAttr = localCollection.attribute(attr->type());
0407             if (localAttr && ignoreAttributeChanges(remoteCollection, attr->type())) {
0408                 continue;
0409             }
0410             // The attribute must both exist and have equal contents
0411             if (!localAttr || localAttr->serialized() != attr->serialized()) {
0412                 return true;
0413             }
0414         }
0415 
0416         return false;
0417     }
0418 
0419     void createLocalCollections()
0420     {
0421         if (remoteCollectionsToCreate.isEmpty()) {
0422             updateLocalCollections();
0423             return;
0424         }
0425 
0426         for (auto iter = remoteCollectionsToCreate.begin(), end = remoteCollectionsToCreate.end(); iter != end;) {
0427             const Collection col = *iter;
0428             const Collection parentCollection = col.parentCollection();
0429             // The parent already exists locally
0430             if (parentCollection == akonadiRootCollection || parentCollection.id() > 0) {
0431                 ++pendingJobs;
0432                 auto create = new CollectionCreateJob(col, currentTransaction);
0433                 QObject::connect(create, &KJob::result, q, [this](KJob *job) {
0434                     createLocalCollectionResult(job);
0435                 });
0436 
0437                 // Commit transaction after every 100 collections are created,
0438                 // otherwise it overloads database journal and things get veeery slow
0439                 if (pendingJobs % 100 == 0) {
0440                     currentTransaction->commit();
0441                     createTransaction();
0442                 }
0443 
0444                 iter = remoteCollectionsToCreate.erase(iter);
0445                 end = remoteCollectionsToCreate.end();
0446             } else {
0447                 // Skip the collection, we'll try again once we create all the other
0448                 // collection we already have a parent for
0449                 ++iter;
0450             }
0451         }
0452     }
0453 
0454     void createLocalCollectionResult(KJob *job)
0455     {
0456         --pendingJobs;
0457         if (job->error()) {
0458             return; // handled by the base class
0459         }
0460 
0461         q->setProcessedAmount(KJob::Bytes, ++progress);
0462 
0463         const Collection newLocal = static_cast<CollectionCreateJob *>(job)->collection();
0464         uidRidMap.insert(newLocal.id(), newLocal.remoteId());
0465         const RemoteId newLocalRID = remoteIdForCollection(newLocal);
0466 
0467         // See if there are any pending collections that this collection is parent of and
0468         // update them if so
0469         for (auto iter = remoteCollectionsToCreate.begin(), end = remoteCollectionsToCreate.end(); iter != end; ++iter) {
0470             const Collection parentCollection = iter->parentCollection();
0471             if (parentCollection != akonadiRootCollection && parentCollection.id() <= 0) {
0472                 const RemoteId remoteRID = remoteIdForCollection(*iter);
0473                 if (remoteRID.isAbsolute()) {
0474                     if (newLocalRID == remoteIdForCollection(*iter)) {
0475                         iter->setParentCollection(newLocal);
0476                     }
0477                 } else if (!hierarchicalRIDs) {
0478                     if (remoteRID.ridChain.startsWith(parentCollection.remoteId())) {
0479                         iter->setParentCollection(newLocal);
0480                     }
0481                 }
0482             }
0483         }
0484 
0485         // Enqueue all pending remote collections that are children of the just-created
0486         // collection
0487         Collection::List collectionsToCreate = remoteCollections.take(newLocalRID);
0488         if (collectionsToCreate.isEmpty() && !hierarchicalRIDs) {
0489             collectionsToCreate = remoteCollections.take(RemoteId(newLocal.remoteId()));
0490         }
0491         for (Collection col : std::as_const(collectionsToCreate)) {
0492             col.setParentCollection(newLocal);
0493             remoteCollectionsToCreate.append(col);
0494         }
0495 
0496         // If there are still any collections to create left, try if we just created
0497         // a parent for any of them
0498         if (!remoteCollectionsToCreate.isEmpty()) {
0499             createLocalCollections();
0500         } else if (pendingJobs == 0) {
0501             Q_ASSERT(remoteCollectionsToCreate.isEmpty());
0502             if (!remoteCollections.isEmpty()) {
0503                 currentTransaction->rollback();
0504                 q->setError(CollectionSync::Unknown);
0505                 q->setErrorText(i18n("Found unresolved orphan collections"));
0506                 qCWarning(AKONADICORE_LOG) << "found unresolved orphan collection";
0507                 emitResult();
0508                 return;
0509             }
0510 
0511             currentTransaction->commit();
0512             createTransaction();
0513 
0514             // Otherwise move to next task: updating existing collections
0515             updateLocalCollections();
0516         }
0517         /*
0518          * else if (!remoteCollections.isEmpty()) {
0519             currentTransaction->rollback();
0520             q->setError(Unknown);
0521             q->setErrorText(i18n("Incomplete collection tree"));
0522             emitResult();
0523             return;
0524         }
0525         */
0526     }
0527 
0528     /**
0529       Performs a local update for the given node pair.
0530     */
0531     void updateLocalCollections()
0532     {
0533         if (remoteCollectionsToUpdate.isEmpty()) {
0534             deleteLocalCollections();
0535             return;
0536         }
0537 
0538         using CollectionPair = QPair<Collection, Collection>;
0539         for (const CollectionPair &pair : std::as_const(remoteCollectionsToUpdate)) {
0540             const Collection local = pair.first;
0541             const Collection remote = pair.second;
0542             Collection upd(remote);
0543 
0544             Q_ASSERT(!upd.remoteId().isEmpty());
0545             Q_ASSERT(currentTransaction);
0546             upd.setId(local.id());
0547             if (ignoreAttributeChanges(remote, CONTENTMIMETYPES)) {
0548                 upd.setContentMimeTypes(local.contentMimeTypes());
0549             }
0550             const auto remoteAttributes = upd.attributes();
0551             for (Attribute *remoteAttr : remoteAttributes) {
0552                 if (ignoreAttributeChanges(remote, remoteAttr->type()) && local.hasAttribute(remoteAttr->type())) {
0553                     // We don't want to overwrite the attribute changes with the defaults provided by the resource.
0554                     const Attribute *localAttr = local.attribute(remoteAttr->type());
0555                     upd.removeAttribute(localAttr->type());
0556                     upd.addAttribute(localAttr->clone());
0557                 }
0558             }
0559 
0560             // ### HACK to work around the implicit move attempts of CollectionModifyJob
0561             // which we do explicitly below
0562             Collection c(upd);
0563             c.setParentCollection(local.parentCollection());
0564             ++pendingJobs;
0565             auto mod = new CollectionModifyJob(c, currentTransaction);
0566             QObject::connect(mod, &KJob::result, q, [this](KJob *job) {
0567                 updateLocalCollectionResult(job);
0568             });
0569 
0570             // detecting moves is only possible with global RIDs
0571             if (!hierarchicalRIDs) {
0572                 if (remote.parentCollection().isValid() && remote.parentCollection().id() != local.parentCollection().id()) {
0573                     ++pendingJobs;
0574                     auto move = new CollectionMoveJob(upd, remote.parentCollection(), currentTransaction);
0575                     QObject::connect(move, &KJob::result, q, [this](KJob *job) {
0576                         updateLocalCollectionResult(job);
0577                     });
0578                 }
0579             }
0580         }
0581     }
0582 
0583     void updateLocalCollectionResult(KJob *job)
0584     {
0585         --pendingJobs;
0586         if (job->error()) {
0587             return; // handled by the base class
0588         }
0589         if (qobject_cast<CollectionModifyJob *>(job)) {
0590             q->setProcessedAmount(KJob::Bytes, ++progress);
0591         }
0592 
0593         // All updates are done, time to move on to next task: deletion
0594         if (pendingJobs == 0) {
0595             currentTransaction->commit();
0596             createTransaction();
0597 
0598             deleteLocalCollections();
0599         }
0600     }
0601 
0602     void deleteLocalCollections()
0603     {
0604         if (localCollectionsToRemove.isEmpty()) {
0605             done();
0606             return;
0607         }
0608 
0609         for (const Collection &col : std::as_const(localCollectionsToRemove)) {
0610             Q_ASSERT(!col.remoteId().isEmpty()); // empty RID -> stuff we haven't even written to the remote side yet
0611 
0612             ++pendingJobs;
0613             Q_ASSERT(currentTransaction);
0614             auto job = new CollectionDeleteJob(col, currentTransaction);
0615             QObject::connect(job, &KJob::result, q, [this](KJob *job) {
0616                 deleteLocalCollectionsResult(job);
0617             });
0618 
0619             // It can happen that the groupware servers report us deleted collections
0620             // twice, in this case this collection delete job will fail on the second try.
0621             // To avoid a rollback of the complete transaction we gracefully allow the job
0622             // to fail :)
0623             currentTransaction->setIgnoreJobFailure(job);
0624         }
0625     }
0626 
0627     void deleteLocalCollectionsResult(KJob * /*unused*/)
0628     {
0629         --pendingJobs;
0630         q->setProcessedAmount(KJob::Bytes, ++progress);
0631 
0632         if (pendingJobs == 0) {
0633             currentTransaction->commit();
0634             currentTransaction = nullptr;
0635 
0636             done();
0637         }
0638     }
0639 
0640     void done()
0641     {
0642         if (currentTransaction) {
0643             // This can trigger a direct call of transactionSequenceResult
0644             currentTransaction->commit();
0645             currentTransaction = nullptr;
0646         }
0647 
0648         if (!remoteCollections.isEmpty()) {
0649             q->setError(CollectionSync::Unknown);
0650             q->setErrorText(i18n("Found unresolved orphan collections"));
0651         }
0652         emitResult();
0653     }
0654 
0655     void emitResult()
0656     {
0657         // Prevent double result emission
0658         Q_ASSERT(!resultEmitted);
0659         if (!resultEmitted) {
0660             if (q->hasSubjobs()) {
0661                 // If there are subjobs, pick one, wait for it to finish, then
0662                 // try again. This way we make sure we don't emit result() signal
0663                 // while there is still a Transaction job running
0664                 KJob *subjob = q->subjobs().at(0);
0665                 QObject::connect(
0666                     subjob,
0667                     &KJob::result,
0668                     q,
0669                     [this](KJob * /*unused*/) {
0670                         emitResult();
0671                     },
0672                     Qt::QueuedConnection);
0673             } else {
0674                 resultEmitted = true;
0675                 q->emitResult();
0676             }
0677         }
0678     }
0679 
0680     void createTransaction()
0681     {
0682         currentTransaction = new TransactionSequence(q);
0683         currentTransaction->setAutomaticCommittingEnabled(false);
0684         q->connect(currentTransaction, &TransactionSequence::finished, q, [this](KJob *job) {
0685             transactionSequenceResult(job);
0686         });
0687     }
0688 
0689     /** After the transaction has finished report we're done as well. */
0690     void transactionSequenceResult(KJob *job)
0691     {
0692         if (job->error()) {
0693             return; // handled by the base class
0694         }
0695 
0696         // If this was the last transaction, then finish, otherwise there's
0697         // a new transaction in the queue already
0698         if (job == currentTransaction) {
0699             currentTransaction = nullptr;
0700         }
0701     }
0702 
0703     /**
0704       Process what's currently available.
0705     */
0706     void execute()
0707     {
0708         qCDebug(AKONADICORE_LOG) << "localListDone: " << localListDone << " deliveryDone: " << deliveryDone;
0709         if (!localListDone && !deliveryDone) {
0710             return;
0711         }
0712 
0713         if (!localListDone && deliveryDone) {
0714             Job *parent = (currentTransaction ? static_cast<Job *>(currentTransaction) : static_cast<Job *>(q));
0715             auto job = new CollectionFetchJob(akonadiRootCollection, CollectionFetchJob::Recursive, parent);
0716             job->fetchScope().setResource(resourceId);
0717             job->fetchScope().setListFilter(CollectionFetchScope::NoFilter);
0718             job->fetchScope().setAncestorRetrieval(CollectionFetchScope::All);
0719             q->connect(job, &CollectionFetchJob::collectionsReceived, q, [this](const auto &cols) {
0720                 localCollectionsReceived(cols);
0721             });
0722             q->connect(job, &KJob::result, q, [this](KJob *job) {
0723                 localCollectionFetchResult(job);
0724             });
0725             return;
0726         }
0727 
0728         // If a transaction is not started yet, it means we just finished local listing
0729         if (!currentTransaction) {
0730             // There's nothing to do after local listing -> we are done!
0731             if (remoteCollectionsToCreate.isEmpty() && remoteCollectionsToUpdate.isEmpty() && localCollectionsToRemove.isEmpty()) {
0732                 qCDebug(AKONADICORE_LOG) << "Nothing to do";
0733                 emitResult();
0734                 return;
0735             }
0736             // Ok, there's some work to do, so create a transaction we can use
0737             createTransaction();
0738         }
0739 
0740         createLocalCollections();
0741     }
0742 
0743     CollectionSync *const q;
0744 
0745     QString resourceId;
0746 
0747     int pendingJobs;
0748     int progress;
0749 
0750     TransactionSequence *currentTransaction;
0751 
0752     bool incremental;
0753     bool streaming;
0754     bool hierarchicalRIDs;
0755 
0756     bool localListDone;
0757     bool deliveryDone;
0758 
0759     // List of parts where local changes should not be overwritten
0760     QSet<QByteArray> keepLocalChanges;
0761 
0762     QHash<RemoteId /* parent */, Collection::List /* children */> removedRemoteCollections;
0763     QHash<RemoteId /* parent */, Collection::List /* children */> remoteCollections;
0764     QHash<RemoteId /* parent */, Collection::List /* children */> localCollections;
0765 
0766     Collection::List localCollectionsToRemove;
0767     Collection::List remoteCollectionsToCreate;
0768     QList<QPair<Collection /* local */, Collection /* remote */>> remoteCollectionsToUpdate;
0769     QHash<Collection::Id, QString> uidRidMap;
0770 
0771     // HACK: To workaround Collection copy constructor being very expensive, we
0772     // store the Collection::root() collection in a variable here for faster
0773     // access
0774     Collection akonadiRootCollection;
0775 
0776     bool resultEmitted;
0777 };
0778 
0779 CollectionSync::CollectionSync(const QString &resourceId, QObject *parent)
0780     : Job(parent)
0781     , d(new CollectionSyncPrivate(this))
0782 {
0783     d->resourceId = resourceId;
0784     setTotalAmount(KJob::Bytes, 0);
0785 }
0786 
0787 CollectionSync::~CollectionSync() = default;
0788 
0789 void CollectionSync::setRemoteCollections(const Collection::List &remoteCollections)
0790 {
0791     setTotalAmount(KJob::Bytes, totalAmount(KJob::Bytes) + remoteCollections.count());
0792     for (const Collection &c : remoteCollections) {
0793         d->addRemoteColection(c);
0794     }
0795 
0796     if (!d->streaming) {
0797         d->deliveryDone = true;
0798     }
0799     d->execute();
0800 }
0801 
0802 void CollectionSync::setRemoteCollections(const Collection::List &changedCollections, const Collection::List &removedCollections)
0803 {
0804     setTotalAmount(KJob::Bytes, totalAmount(KJob::Bytes) + changedCollections.count());
0805     d->incremental = true;
0806     for (const Collection &c : changedCollections) {
0807         d->addRemoteColection(c);
0808     }
0809     for (const Collection &c : removedCollections) {
0810         d->addRemoteColection(c, true);
0811     }
0812 
0813     if (!d->streaming) {
0814         d->deliveryDone = true;
0815     }
0816     d->execute();
0817 }
0818 
0819 void CollectionSync::doStart()
0820 {
0821 }
0822 
0823 void CollectionSync::setStreamingEnabled(bool streaming)
0824 {
0825     d->streaming = streaming;
0826 }
0827 
0828 void CollectionSync::retrievalDone()
0829 {
0830     d->deliveryDone = true;
0831     d->execute();
0832 }
0833 
0834 void CollectionSync::setHierarchicalRemoteIds(bool hierarchical)
0835 {
0836     d->hierarchicalRIDs = hierarchical;
0837 }
0838 
0839 void CollectionSync::rollback()
0840 {
0841     if (d->currentTransaction) {
0842         d->currentTransaction->rollback();
0843     } else {
0844         setError(UserCanceled);
0845         emitResult();
0846     }
0847 }
0848 
0849 void CollectionSync::setKeepLocalChanges(const QSet<QByteArray> &parts)
0850 {
0851     d->keepLocalChanges = parts;
0852 }
0853 
0854 #include "moc_collectionsync_p.cpp"