File indexing completed on 2025-01-05 04:46:59
0001 /* 0002 SPDX-FileCopyrightText: 2009 Volker Krause <vkrause@kde.org> 0003 SPDX-FileCopyrightText: 2010 Milian Wolff <mail@milianw.de> 0004 0005 SPDX-License-Identifier: LGPL-2.0-or-later 0006 */ 0007 0008 #include "itemretriever.h" 0009 0010 #include "akonadi.h" 0011 #include "connection.h" 0012 #include "storage/datastore.h" 0013 #include "storage/itemqueryhelper.h" 0014 #include "storage/itemretrievalmanager.h" 0015 #include "storage/itemretrievalrequest.h" 0016 #include "storage/parthelper.h" 0017 #include "storage/parttypehelper.h" 0018 #include "storage/querybuilder.h" 0019 #include "storage/selectquerybuilder.h" 0020 #include "utils.h" 0021 0022 #include "private/protocol_p.h" 0023 #include "shared/akranges.h" 0024 0025 #include <QEventLoop> 0026 0027 #include "akonadiserver_debug.h" 0028 0029 using namespace Akonadi; 0030 using namespace Akonadi::Server; 0031 using namespace AkRanges; 0032 0033 Q_DECLARE_METATYPE(ItemRetrievalResult) 0034 0035 ItemRetriever::ItemRetriever(ItemRetrievalManager &manager, Connection *connection, const CommandContext &context) 0036 : mItemRetrievalManager(manager) 0037 , mConnection(connection) 0038 , mContext(context) 0039 , mFullPayload(false) 0040 , mRecursive(false) 0041 , mCanceled(false) 0042 { 0043 qRegisterMetaType<ItemRetrievalResult>("Akonadi::Server::ItemRetrievalResult"); 0044 if (mConnection) { 0045 connect(mConnection, &Connection::disconnected, this, [this]() { 0046 mCanceled = true; 0047 }); 0048 } 0049 } 0050 0051 Connection *ItemRetriever::connection() const 0052 { 0053 return mConnection; 0054 } 0055 0056 void ItemRetriever::setRetrieveParts(const QList<QByteArray> &parts) 0057 { 0058 mParts = parts; 0059 std::sort(mParts.begin(), mParts.end()); 0060 mParts.erase(std::unique(mParts.begin(), mParts.end()), mParts.end()); 0061 0062 // HACK, we need a full payload available flag in PimItem 0063 if (mFullPayload && !mParts.contains(AKONADI_PARAM_PLD_RFC822)) { 0064 mParts.append(AKONADI_PARAM_PLD_RFC822); 0065 } 0066 } 0067 0068 void ItemRetriever::setItemSet(const ImapSet &set, const Collection &collection) 0069 { 0070 mItemSet = set; 0071 mCollection = collection; 0072 } 0073 0074 void ItemRetriever::setItemSet(const ImapSet &set, bool isUid) 0075 { 0076 if (!isUid && mContext.collectionId() >= 0) { 0077 setItemSet(set, mContext.collection()); 0078 } else { 0079 setItemSet(set); 0080 } 0081 } 0082 0083 void ItemRetriever::setItem(Entity::Id id) 0084 { 0085 ImapSet set; 0086 set.add(ImapInterval(id, id)); 0087 mItemSet = set; 0088 mCollection = Collection(); 0089 } 0090 0091 void ItemRetriever::setRetrieveFullPayload(bool fullPayload) 0092 { 0093 mFullPayload = fullPayload; 0094 // HACK, we need a full payload available flag in PimItem 0095 if (fullPayload && !mParts.contains(AKONADI_PARAM_PLD_RFC822)) { 0096 mParts.append(AKONADI_PARAM_PLD_RFC822); 0097 } 0098 } 0099 0100 void ItemRetriever::setCollection(const Collection &collection, bool recursive) 0101 { 0102 mCollection = collection; 0103 mItemSet = ImapSet(); 0104 mRecursive = recursive; 0105 } 0106 0107 void ItemRetriever::setScope(const Scope &scope) 0108 { 0109 mScope = scope; 0110 } 0111 0112 Scope ItemRetriever::scope() const 0113 { 0114 return mScope; 0115 } 0116 0117 void ItemRetriever::setChangedSince(const QDateTime &changedSince) 0118 { 0119 mChangedSince = changedSince; 0120 } 0121 0122 QList<QByteArray> ItemRetriever::retrieveParts() const 0123 { 0124 return mParts; 0125 } 0126 0127 enum QueryColumns { 0128 PimItemIdColumn, 0129 0130 CollectionIdColumn, 0131 ResourceIdColumn, 0132 0133 PartTypeNameColumn, 0134 PartDatasizeColumn 0135 }; 0136 0137 QSqlQuery ItemRetriever::buildQuery() const 0138 { 0139 QueryBuilder qb(PimItem::tableName()); 0140 0141 qb.addJoin(QueryBuilder::InnerJoin, Collection::tableName(), PimItem::collectionIdFullColumnName(), Collection::idFullColumnName()); 0142 0143 qb.addJoin(QueryBuilder::LeftJoin, Part::tableName(), PimItem::idFullColumnName(), Part::pimItemIdFullColumnName()); 0144 0145 Query::Condition partTypeJoinCondition; 0146 partTypeJoinCondition.addColumnCondition(Part::partTypeIdFullColumnName(), Query::Equals, PartType::idFullColumnName()); 0147 if (!mFullPayload && !mParts.isEmpty()) { 0148 partTypeJoinCondition.addCondition(PartTypeHelper::conditionFromFqNames(mParts)); 0149 } 0150 partTypeJoinCondition.addValueCondition(PartType::nsFullColumnName(), Query::Equals, QStringLiteral("PLD")); 0151 qb.addJoin(QueryBuilder::LeftJoin, PartType::tableName(), partTypeJoinCondition); 0152 0153 qb.addColumn(PimItem::idFullColumnName()); 0154 qb.addColumn(PimItem::collectionIdFullColumnName()); 0155 qb.addColumn(Collection::resourceIdFullColumnName()); 0156 qb.addColumn(PartType::nameFullColumnName()); 0157 qb.addColumn(Part::datasizeFullColumnName()); 0158 0159 if (!mItemSet.isEmpty() || mCollection.isValid()) { 0160 ItemQueryHelper::itemSetToQuery(mItemSet, qb, mCollection); 0161 } else { 0162 ItemQueryHelper::scopeToQuery(mScope, mContext, qb); 0163 } 0164 0165 // prevent a resource to trigger item retrieval from itself 0166 if (mConnection) { 0167 const Resource res = Resource::retrieveByName(QString::fromUtf8(mConnection->sessionId())); 0168 if (res.isValid()) { 0169 qb.addValueCondition(Collection::resourceIdFullColumnName(), Query::NotEquals, res.id()); 0170 } 0171 } 0172 0173 if (mChangedSince.isValid()) { 0174 qb.addValueCondition(PimItem::datetimeFullColumnName(), Query::GreaterOrEqual, mChangedSince.toUTC()); 0175 } 0176 0177 qb.addSortColumn(PimItem::idFullColumnName(), Query::Ascending); 0178 0179 if (!qb.exec()) { 0180 mLastError = "Unable to retrieve items"; 0181 throw ItemRetrieverException(mLastError); 0182 } 0183 0184 qb.query().next(); 0185 0186 return qb.query(); 0187 } 0188 0189 namespace 0190 { 0191 bool hasAllParts(const ItemRetrievalRequest &req, const QSet<QByteArray> &availableParts) 0192 { 0193 return std::all_of(req.parts.begin(), req.parts.end(), [&availableParts](const auto &part) { 0194 return availableParts.contains(part); 0195 }); 0196 } 0197 } 0198 0199 bool ItemRetriever::runItemRetrievalRequests(std::list<ItemRetrievalRequest> requests) // clazy:exclude=function-args-by-ref 0200 { 0201 QEventLoop eventLoop; 0202 std::vector<ItemRetrievalRequest::Id> pendingRequests; 0203 connect(&mItemRetrievalManager, 0204 &ItemRetrievalManager::requestFinished, 0205 this, 0206 [this, &eventLoop, &pendingRequests](const ItemRetrievalResult &result) { // clazy:exclude=lambda-in-connect 0207 const auto requestId = std::find(pendingRequests.begin(), pendingRequests.end(), result.request.id); 0208 if (requestId != pendingRequests.end()) { 0209 if (mCanceled) { 0210 eventLoop.exit(1); 0211 } else if (result.errorMsg.has_value()) { 0212 mLastError = result.errorMsg->toUtf8(); 0213 eventLoop.exit(1); 0214 } else { 0215 Q_EMIT itemsRetrieved(result.request.ids); 0216 pendingRequests.erase(requestId); 0217 if (pendingRequests.empty()) { 0218 eventLoop.quit(); 0219 } 0220 } 0221 } 0222 }); 0223 0224 if (mConnection) { 0225 connect(mConnection, &Connection::connectionClosing, &eventLoop, [&eventLoop]() { 0226 eventLoop.exit(1); 0227 }); 0228 } 0229 0230 for (auto &&request : requests) { 0231 if ((!mFullPayload && request.parts.isEmpty()) || request.ids.isEmpty()) { 0232 continue; 0233 } 0234 0235 // TODO: how should we handle retrieval errors here? so far they have been ignored, 0236 // which makes sense in some cases, do we need a command parameter for this? 0237 try { 0238 // Request is deleted inside ItemRetrievalManager, so we need to take 0239 // a copy here 0240 // const auto ids = request->ids; 0241 pendingRequests.push_back(request.id); 0242 mItemRetrievalManager.requestItemDelivery(std::move(request)); 0243 } catch (const ItemRetrieverException &e) { 0244 qCCritical(AKONADISERVER_LOG) << e.type() << ": " << e.what(); 0245 mLastError = e.what(); 0246 return false; 0247 } 0248 } 0249 0250 if (!pendingRequests.empty()) { 0251 if (eventLoop.exec()) { 0252 return false; 0253 } 0254 } 0255 0256 return true; 0257 } 0258 0259 std::optional<ItemRetriever::PreparedRequests> ItemRetriever::prepareRequests(QSqlQuery &query, const QByteArrayList &parts) 0260 { 0261 QHash<qint64, QString> resourceIdNameCache; 0262 std::list<ItemRetrievalRequest> requests; 0263 QHash<qint64 /* collection */, decltype(requests)::iterator> colRequests; 0264 QHash<qint64 /* item */, decltype(requests)::iterator> itemRequests; 0265 QList<qint64> readyItems; 0266 qint64 prevPimItemId = -1; 0267 QSet<QByteArray> availableParts; 0268 auto lastRequest = requests.end(); 0269 while (query.isValid()) { 0270 const qint64 pimItemId = query.value(PimItemIdColumn).toLongLong(); 0271 const qint64 collectionId = query.value(CollectionIdColumn).toLongLong(); 0272 const qint64 resourceId = query.value(ResourceIdColumn).toLongLong(); 0273 const auto itemIter = itemRequests.constFind(pimItemId); 0274 0275 if (Q_UNLIKELY(mCanceled)) { 0276 return std::nullopt; 0277 } 0278 0279 if (pimItemId == prevPimItemId) { 0280 if (query.value(PartTypeNameColumn).isNull()) { 0281 // This is not the first part of the Item we saw, but LEFT JOIN PartTable 0282 // returned a null row - that means the row is an ATR part 0283 // which we don't care about 0284 query.next(); 0285 continue; 0286 } 0287 } else { 0288 if (lastRequest != requests.end()) { 0289 if (hasAllParts(*lastRequest, availableParts)) { 0290 // We went through all parts of a single item, if we have all 0291 // parts available in the DB and they are not expired, then 0292 // exclude this item from the retrieval 0293 lastRequest->ids.removeOne(prevPimItemId); 0294 itemRequests.remove(prevPimItemId); 0295 readyItems.push_back(prevPimItemId); 0296 } 0297 } 0298 availableParts.clear(); 0299 prevPimItemId = pimItemId; 0300 } 0301 0302 if (itemIter != itemRequests.constEnd()) { 0303 lastRequest = itemIter.value(); 0304 } else { 0305 const auto colIt = colRequests.find(collectionId); 0306 lastRequest = (colIt == colRequests.end()) ? requests.end() : colIt.value(); 0307 if (lastRequest == requests.end() || lastRequest->ids.size() > 100) { 0308 requests.emplace_front(ItemRetrievalRequest{}); 0309 lastRequest = requests.begin(); 0310 lastRequest->ids.push_back(pimItemId); 0311 auto resIter = resourceIdNameCache.find(resourceId); 0312 if (resIter == resourceIdNameCache.end()) { 0313 resIter = resourceIdNameCache.insert(resourceId, Resource::retrieveById(resourceId).name()); 0314 } 0315 lastRequest->resourceId = *resIter; 0316 lastRequest->parts = parts; 0317 colRequests.insert(collectionId, lastRequest); 0318 itemRequests.insert(pimItemId, lastRequest); 0319 } else { 0320 lastRequest->ids.push_back(pimItemId); 0321 itemRequests.insert(pimItemId, lastRequest); 0322 colRequests.insert(collectionId, lastRequest); 0323 } 0324 } 0325 Q_ASSERT(lastRequest != requests.end()); 0326 0327 if (query.value(PartTypeNameColumn).isNull()) { 0328 // LEFT JOIN did not find anything, retrieve all parts 0329 query.next(); 0330 continue; 0331 } 0332 0333 qint64 datasize = query.value(PartDatasizeColumn).toLongLong(); 0334 const QByteArray partName = Utils::variantToByteArray(query.value(PartTypeNameColumn)); 0335 Q_ASSERT(!partName.startsWith(AKONADI_PARAM_PLD)); 0336 if (datasize <= 0) { 0337 // request update for this part 0338 if (mFullPayload && !lastRequest->parts.contains(partName)) { 0339 lastRequest->parts.push_back(partName); 0340 } 0341 } else { 0342 // add the part to list of available parts, we will compare it with 0343 // the list of request parts once we handle all parts of this item 0344 availableParts.insert(partName); 0345 } 0346 query.next(); 0347 } 0348 query.finish(); 0349 0350 // Post-check in case we only queried one item thus did not reach the check 0351 // at the beginning of the while() loop above 0352 if (lastRequest != requests.end() && hasAllParts(*lastRequest, availableParts)) { 0353 lastRequest->ids.removeOne(prevPimItemId); 0354 readyItems.push_back(prevPimItemId); 0355 // No need to update the hashtable at this point 0356 } 0357 0358 return PreparedRequests{std::move(requests), std::move(readyItems)}; 0359 } 0360 0361 bool ItemRetriever::exec() 0362 { 0363 if (mParts.isEmpty() && !mFullPayload) { 0364 return true; 0365 } 0366 0367 verifyCache(); 0368 0369 QSqlQuery query = buildQuery(); 0370 const auto parts = mParts | Views::filter([](const auto &part) { 0371 return part.startsWith(AKONADI_PARAM_PLD); 0372 }) 0373 | Views::transform([](const auto &part) { 0374 return part.mid(4); 0375 }) 0376 | Actions::toQList; 0377 0378 auto requests = prepareRequests(query, parts); 0379 if (!requests.has_value()) { 0380 return false; 0381 } 0382 0383 if (!requests->readyItems.isEmpty()) { 0384 Q_EMIT itemsRetrieved(requests->readyItems); 0385 } 0386 0387 if (!runItemRetrievalRequests(std::move(requests->requests))) { 0388 return false; 0389 } 0390 0391 // retrieve items in child collections if requested 0392 bool result = true; 0393 if (mRecursive && mCollection.isValid()) { 0394 const auto children = mCollection.children(); 0395 for (const Collection &col : children) { 0396 ItemRetriever retriever(mItemRetrievalManager, mConnection, mContext); 0397 retriever.setCollection(col, mRecursive); 0398 retriever.setRetrieveParts(mParts); 0399 retriever.setRetrieveFullPayload(mFullPayload); 0400 connect(&retriever, &ItemRetriever::itemsRetrieved, this, &ItemRetriever::itemsRetrieved); 0401 result = retriever.exec(); 0402 if (!result) { 0403 break; 0404 } 0405 } 0406 } 0407 0408 return result; 0409 } 0410 0411 void ItemRetriever::verifyCache() 0412 { 0413 if (!connection() || !connection()->verifyCacheOnRetrieval()) { 0414 return; 0415 } 0416 0417 SelectQueryBuilder<Part> qb; 0418 qb.addJoin(QueryBuilder::InnerJoin, PimItem::tableName(), Part::pimItemIdFullColumnName(), PimItem::idFullColumnName()); 0419 qb.addValueCondition(Part::storageFullColumnName(), Query::Equals, Part::External); 0420 qb.addValueCondition(Part::dataFullColumnName(), Query::IsNot, QVariant()); 0421 if (mScope.scope() != Scope::Invalid) { 0422 ItemQueryHelper::scopeToQuery(mScope, mContext, qb); 0423 } else { 0424 ItemQueryHelper::itemSetToQuery(mItemSet, qb, mCollection); 0425 } 0426 0427 if (!qb.exec()) { 0428 mLastError = QByteArrayLiteral("Unable to query parts."); 0429 throw ItemRetrieverException(mLastError); 0430 } 0431 0432 const Part::List externalParts = qb.result(); 0433 for (Part part : externalParts) { 0434 PartHelper::verify(part); 0435 } 0436 } 0437 0438 QByteArray ItemRetriever::lastError() const 0439 { 0440 return mLastError; 0441 } 0442 0443 #include "moc_itemretriever.cpp"