diff --git a/src/server/handler/akappend.cpp b/src/server/handler/akappend.cpp index 244050631..fc1db90f9 100644 --- a/src/server/handler/akappend.cpp +++ b/src/server/handler/akappend.cpp @@ -1,421 +1,420 @@ /*************************************************************************** * Copyright (C) 2007 by Robert Zwerus * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "akappend.h" #include "fetchhelper.h" #include "connection.h" #include "preprocessormanager.h" #include "handlerhelper.h" #include "storage/datastore.h" #include "storage/transaction.h" #include "storage/parttypehelper.h" #include "storage/dbconfig.h" #include "storage/partstreamer.h" #include "storage/parthelper.h" #include "storage/selectquerybuilder.h" #include #include //std::accumulate using namespace Akonadi; using namespace Akonadi::Server; static QVector localFlagsToPreserve = QVector() << "$ATTACHMENT" << "$INVITATION" << "$ENCRYPTED" << "$SIGNED" << "$WATCHED"; bool AkAppend::buildPimItem(const Protocol::CreateItemCommand &cmd, PimItem &item, Collection &parentCol) { parentCol = HandlerHelper::collectionFromScope(cmd.collection(), connection()); if (!parentCol.isValid()) { return failureResponse("Invalid parent collection"); } if (parentCol.isVirtual()) { return failureResponse("Cannot append item into virtual collection"); } MimeType mimeType = MimeType::retrieveByName(cmd.mimeType()); if (!mimeType.isValid()) { MimeType m(cmd.mimeType()); if (!m.insert()) { return failureResponse(QStringLiteral("Unable to create mimetype '") % cmd.mimeType() % QStringLiteral("'.")); } mimeType = m; } item.setRev(0); item.setSize(cmd.itemSize()); item.setMimeTypeId(mimeType.id()); item.setCollectionId(parentCol.id()); item.setDatetime(cmd.dateTime().isValid() ? cmd.dateTime() : QDateTime::currentDateTimeUtc()); if (cmd.remoteId().isEmpty()) { // from application item.setDirty(true); } else { // from resource item.setRemoteId(cmd.remoteId()); item.setDirty(false); } item.setRemoteRevision(cmd.remoteRevision()); item.setGid(cmd.gid()); item.setAtime(QDateTime::currentDateTime()); return true; } bool AkAppend::insertItem(const Protocol::CreateItemCommand &cmd, PimItem &item, const Collection &parentCol) { if (!item.insert()) { return failureResponse("Failed to append item"); } // set message flags const QSet flags = cmd.mergeModes() == Protocol::CreateItemCommand::None ? cmd.flags() : cmd.addedFlags(); if (!flags.isEmpty()) { // This will hit an entry in cache inserted there in buildPimItem() const Flag::List flagList = HandlerHelper::resolveFlags(flags); bool flagsChanged = false; if (!DataStore::self()->appendItemsFlags(PimItem::List() << item, flagList, &flagsChanged, false, parentCol, true)) { return failureResponse("Unable to append item flags."); } } const Scope tags = cmd.mergeModes() == Protocol::CreateItemCommand::None ? cmd.tags() : cmd.addedTags(); if (!tags.isEmpty()) { const Tag::List tagList = HandlerHelper::tagsFromScope(tags, connection()); bool tagsChanged = false; if (!DataStore::self()->appendItemsTags(PimItem::List() << item, tagList, &tagsChanged, false, parentCol, true)) { return failureResponse("Unable to append item tags."); } } // Handle individual parts qint64 partSizes = 0; PartStreamer streamer(connection(), item, this); connect(&streamer, &PartStreamer::responseAvailable, this, static_cast(&Handler::sendResponse)); Q_FOREACH (const QByteArray &partName, cmd.parts()) { qint64 partSize = 0; if (!streamer.stream(true, partName, partSize)) { return failureResponse(streamer.error()); } partSizes += partSize; } const Protocol::Attributes attrs = cmd.attributes(); for (auto iter = attrs.cbegin(), end = attrs.cend(); iter != end; ++iter) { if (!streamer.streamAttribute(true, iter.key(), iter.value())) { return failureResponse(streamer.error()); } } // TODO: Try to avoid this addition query if (partSizes > item.size()) { item.setSize(partSizes); item.update(); } // Preprocessing if (PreprocessorManager::instance()->isActive()) { Part hiddenAttribute; hiddenAttribute.setPimItemId(item.id()); hiddenAttribute.setPartType(PartTypeHelper::fromFqName(QStringLiteral(AKONADI_ATTRIBUTE_HIDDEN))); hiddenAttribute.setData(QByteArray()); hiddenAttribute.setDatasize(0); // TODO: Handle errors? Technically, this is not a critical issue as no data are lost PartHelper::insert(&hiddenAttribute); } notify(item, item.collection()); sendResponse(item, Protocol::CreateItemCommand::None); return true; } bool AkAppend::mergeItem(const Protocol::CreateItemCommand &cmd, PimItem &newItem, PimItem ¤tItem, const Collection &parentCol) { QSet changedParts; if (newItem.rev() > 0) { currentItem.setRev(newItem.rev()); } if (!newItem.remoteId().isEmpty() && currentItem.remoteId() != newItem.remoteId()) { currentItem.setRemoteId(newItem.remoteId()); changedParts.insert(AKONADI_PARAM_REMOTEID); } if (!newItem.remoteRevision().isEmpty() && currentItem.remoteRevision() != newItem.remoteRevision()) { currentItem.setRemoteRevision(newItem.remoteRevision()); changedParts.insert(AKONADI_PARAM_REMOTEREVISION); } if (!newItem.gid().isEmpty() && currentItem.gid() != newItem.gid()) { currentItem.setGid(newItem.gid()); changedParts.insert(AKONADI_PARAM_GID); } if (newItem.datetime().isValid()) { currentItem.setDatetime(newItem.datetime()); } currentItem.setAtime(QDateTime::currentDateTime()); currentItem.setSize(newItem.size()); // Only mark dirty when merged from application currentItem.setDirty(!connection()->context()->resource().isValid()); const Collection col = Collection::retrieveById(parentCol.id()); if (cmd.flags().isEmpty()) { bool flagsAdded = false, flagsRemoved = false; if (!cmd.addedFlags().isEmpty()) { const Flag::List addedFlags = HandlerHelper::resolveFlags(cmd.addedFlags()); DataStore::self()->appendItemsFlags(PimItem::List() << currentItem, addedFlags, &flagsAdded, true, col, true); } if (!cmd.removedFlags().isEmpty()) { const Flag::List removedFlags = HandlerHelper::resolveFlags(cmd.removedFlags()); DataStore::self()->removeItemsFlags(PimItem::List() << currentItem, removedFlags, &flagsRemoved, col, true); } if (flagsAdded || flagsRemoved) { changedParts.insert(AKONADI_PARAM_FLAGS); } } else if (!cmd.flags().isEmpty()) { bool flagsChanged = false; QSet flagNames = cmd.flags(); // Make sure we don't overwrite some local-only flags that can't come // through from Resource during ItemSync, like $ATTACHMENT, because the // resource is not aware of them (they are usually assigned by client // upon inspecting the payload) Q_FOREACH (const QByteArray &preserve, localFlagsToPreserve) { flagNames.remove(preserve); } const Flag::List flags = HandlerHelper::resolveFlags(flagNames); DataStore::self()->setItemsFlags(PimItem::List() << currentItem, flags, &flagsChanged, col, true); if (flagsChanged) { changedParts.insert(AKONADI_PARAM_FLAGS); } } if (cmd.tags().isEmpty()) { bool tagsAdded = false, tagsRemoved = false; if (!cmd.addedTags().isEmpty()) { const Tag::List addedTags = HandlerHelper::tagsFromScope(cmd.addedTags(), connection()); DataStore::self()->appendItemsTags(PimItem::List() << currentItem, addedTags, &tagsAdded, true, col, true); } if (!cmd.removedTags().isEmpty()) { const Tag::List removedTags = HandlerHelper::tagsFromScope(cmd.removedTags(), connection()); DataStore::self()->removeItemsTags(PimItem::List() << currentItem, removedTags, &tagsRemoved, true); } if (tagsAdded || tagsRemoved) { changedParts.insert(AKONADI_PARAM_TAGS); } } else if (!cmd.tags().isEmpty()) { bool tagsChanged = false; const Tag::List tags = HandlerHelper::tagsFromScope(cmd.tags(), connection()); DataStore::self()->setItemsTags(PimItem::List() << currentItem, tags, &tagsChanged, true); if (tagsChanged) { changedParts.insert(AKONADI_PARAM_TAGS); } } const Part::List existingParts = Part::retrieveFiltered(Part::pimItemIdColumn(), currentItem.id()); QMap partsSizes; Q_FOREACH (const Part &part, existingParts ) { partsSizes.insert(PartTypeHelper::fullName(part.partType()).toLatin1(), part.datasize()); } PartStreamer streamer(connection(), currentItem); connect(&streamer, &PartStreamer::responseAvailable, this, static_cast(&Handler::sendResponse)); Q_FOREACH (const QByteArray &partName, cmd.parts()) { bool changed = false; qint64 partSize = 0; if (!streamer.stream(true, partName, partSize, &changed)) { return failureResponse(streamer.error()); } if (changed) { changedParts.insert(partName); partsSizes.insert(partName, partSize); } } const qint64 size = std::accumulate(partsSizes.begin(), partsSizes.end(), 0); if (size > currentItem.size()) { currentItem.setSize(size); } // Store all changes if (!currentItem.update()) { return failureResponse("Failed to store merged item"); } notify(currentItem, currentItem.collection(), changedParts); sendResponse(currentItem, cmd.mergeModes()); return true; } bool AkAppend::sendResponse(const PimItem& item, Protocol::CreateItemCommand::MergeModes mergeModes) { if (mergeModes & Protocol::CreateItemCommand::Silent || mergeModes & Protocol::CreateItemCommand::None) { Protocol::FetchItemsResponse resp(item.id()); resp.setMTime(item.datetime()); Handler::sendResponse(resp); return true; } Protocol::FetchScope fetchScope; fetchScope.setAncestorDepth(Protocol::Ancestor::ParentAncestor); fetchScope.setFetch(Protocol::FetchScope::AllAttributes | Protocol::FetchScope::FullPayload | Protocol::FetchScope::CacheOnly | Protocol::FetchScope::Flags | Protocol::FetchScope::GID | Protocol::FetchScope::MTime | Protocol::FetchScope::RemoteID | Protocol::FetchScope::RemoteRevision | Protocol::FetchScope::Size | Protocol::FetchScope::Tags); fetchScope.setTagFetchScope({ "GID" }); ImapSet set; set.add(QVector() << item.id()); Scope scope; scope.setUidSet(set); FetchHelper fetchHelper(connection(), scope, fetchScope); if (!fetchHelper.fetchItems()) { return failureResponse("Failed to retrieve item"); } return true; } bool AkAppend::notify(const PimItem &item, const Collection &collection) { DataStore::self()->notificationCollector()->itemAdded(item, collection); if (PreprocessorManager::instance()->isActive()) { // enqueue the item for preprocessing PreprocessorManager::instance()->beginHandleItem(item, DataStore::self()); } return true; } bool AkAppend::notify(const PimItem &item, const Collection &collection, const QSet &changedParts) { if (!changedParts.isEmpty()) { DataStore::self()->notificationCollector()->itemChanged(item, changedParts, collection); } return true; } bool AkAppend::parseStream() { Protocol::CreateItemCommand cmd(m_command); // FIXME: The streaming/reading of all item parts can hold the transaction for // unnecessary long time -> should we wrap the PimItem into one transaction // and try to insert Parts independently? In case we fail to insert a part, // it's not a problem as it can be re-fetched at any time, except for attributes. DataStore *db = DataStore::self(); Transaction transaction(db); ExternalPartStorageTransaction storageTrx; PimItem item; Collection parentCol; if (!buildPimItem(cmd, item, parentCol)) { return false; } if (cmd.mergeModes() == Protocol::CreateItemCommand::None) { if (!insertItem(cmd, item, parentCol)) { return false; } if (!transaction.commit()) { return failureResponse("Failed to commit transaction"); } storageTrx.commit(); } else { - // Merging is always restricted to the same collection and mimetype + // Merging is always restricted to the same collection SelectQueryBuilder qb; qb.addValueCondition(PimItem::collectionIdColumn(), Query::Equals, parentCol.id()); - qb.addValueCondition(PimItem::mimeTypeIdColumn(), Query::Equals, item.mimeTypeId()); if (cmd.mergeModes() & Protocol::CreateItemCommand::GID) { qb.addValueCondition(PimItem::gidColumn(), Query::Equals, item.gid()); } if (cmd.mergeModes() & Protocol::CreateItemCommand::RemoteID) { qb.addValueCondition(PimItem::remoteIdColumn(), Query::Equals, item.remoteId()); } if (!qb.exec()) { return failureResponse("Failed to query database for item"); } const QVector result = qb.result(); if (result.count() == 0) { // No item with such GID/RID exists, so call AkAppend::insert() and behave // like if this was a new item if (!insertItem(cmd, item, parentCol)) { return false; } if (!transaction.commit()) { return failureResponse("Failed to commit transaction"); } storageTrx.commit(); } else if (result.count() == 1) { // Item with matching GID/RID combination exists, so merge this item into it // and send itemChanged() PimItem existingItem = result.at(0); if (!mergeItem(cmd, item, existingItem, parentCol)) { return false; } if (!transaction.commit()) { return failureResponse("Failed to commit transaction"); } storageTrx.commit(); } else { akDebug() << "Multiple merge candidates:"; Q_FOREACH (const PimItem &item, result) { akDebug() << "\t ID: " << item.id() << ", RID:" << item.remoteId() << ", GID:" << item.gid(); } // Nor GID or RID are guaranteed to be unique, so make sure we don't merge // something we don't want return failureResponse("Multiple merge candidates, aborting"); } } return successResponse(); }