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"