summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristian Mollekopf <chrigi_1@fastmail.fm>2015-12-19 19:39:53 +0100
committerChristian Mollekopf <chrigi_1@fastmail.fm>2015-12-20 13:27:44 +0100
commitb1e5525be34850ef4d11cccbf23e118c93e93506 (patch)
tree68283fe123be3b1670eccc6a8e3711a9dda3f35e
parent355ed15b1200dfb6ab5ecd320a3278105007ad9c (diff)
downloadsink-b1e5525be34850ef4d11cccbf23e118c93e93506.tar.gz
sink-b1e5525be34850ef4d11cccbf23e118c93e93506.zip
Generalized the sync algorithms and applied them to mail.
Not necessarily the smartest algorithms, but at least they work and are generally applicable.
-rw-r--r--examples/maildirresource/maildirresource.cpp170
-rw-r--r--examples/maildirresource/maildirresource.h28
-rw-r--r--tests/maildirresourcetest.cpp43
3 files changed, 155 insertions, 86 deletions
diff --git a/examples/maildirresource/maildirresource.cpp b/examples/maildirresource/maildirresource.cpp
index 10e9046..90b4e55 100644
--- a/examples/maildirresource/maildirresource.cpp
+++ b/examples/maildirresource/maildirresource.cpp
@@ -114,7 +114,7 @@ QStringList MaildirResource::listAvailableFolders()
114 return folderList; 114 return folderList;
115} 115}
116 116
117static void createEntity(const QByteArray &akonadiId, const QByteArray &bufferType, Akonadi2::ApplicationDomain::ApplicationDomainType &domainObject, DomainTypeAdaptorFactoryInterface &adaptorFactory, std::function<void(const QByteArray &)> callback) 117static void createEntity(const QByteArray &akonadiId, const QByteArray &bufferType, const Akonadi2::ApplicationDomain::ApplicationDomainType &domainObject, DomainTypeAdaptorFactoryInterface &adaptorFactory, std::function<void(const QByteArray &)> callback)
118{ 118{
119 flatbuffers::FlatBufferBuilder entityFbb; 119 flatbuffers::FlatBufferBuilder entityFbb;
120 adaptorFactory.createBuffer(domainObject, entityFbb); 120 adaptorFactory.createBuffer(domainObject, entityFbb);
@@ -128,7 +128,7 @@ static void createEntity(const QByteArray &akonadiId, const QByteArray &bufferTy
128 callback(QByteArray::fromRawData(reinterpret_cast<char const *>(fbb.GetBufferPointer()), fbb.GetSize())); 128 callback(QByteArray::fromRawData(reinterpret_cast<char const *>(fbb.GetBufferPointer()), fbb.GetSize()));
129} 129}
130 130
131static void modifyEntity(const QByteArray &akonadiId, qint64 revision, const QByteArray &bufferType, Akonadi2::ApplicationDomain::ApplicationDomainType &domainObject, DomainTypeAdaptorFactoryInterface &adaptorFactory, std::function<void(const QByteArray &)> callback) 131static void modifyEntity(const QByteArray &akonadiId, qint64 revision, const QByteArray &bufferType, const Akonadi2::ApplicationDomain::ApplicationDomainType &domainObject, DomainTypeAdaptorFactoryInterface &adaptorFactory, std::function<void(const QByteArray &)> callback)
132{ 132{
133 flatbuffers::FlatBufferBuilder entityFbb; 133 flatbuffers::FlatBufferBuilder entityFbb;
134 adaptorFactory.createBuffer(domainObject, entityFbb); 134 adaptorFactory.createBuffer(domainObject, entityFbb);
@@ -172,40 +172,79 @@ static QSharedPointer<Akonadi2::ApplicationDomain::BufferAdaptor> getLatest(cons
172 return current; 172 return current;
173} 173}
174 174
175void MaildirResource::synchronizeFolders(Akonadi2::Storage::Transaction &transaction) 175void MaildirResource::scanForRemovals(Akonadi2::Storage::Transaction &transaction, Akonadi2::Storage::Transaction &synchronizationTransaction, const QByteArray &bufferType, std::function<bool(const QByteArray &remoteId)> exists)
176{ 176{
177 const QByteArray bufferType = ENTITY_TYPE_FOLDER;
178 QStringList folderList = listAvailableFolders();
179 Trace() << "Found folders " << folderList;
180
181 Akonadi2::Storage store(Akonadi2::storageLocation(), mResourceInstanceIdentifier + ".synchronization", Akonadi2::Storage::ReadWrite);
182 auto synchronizationTransaction = store.createTransaction(Akonadi2::Storage::ReadWrite);
183 auto mainDatabase = transaction.openDatabase(bufferType + ".main"); 177 auto mainDatabase = transaction.openDatabase(bufferType + ".main");
184
185 //TODO Instead of iterating over all entries in the database, which can also pick up the same item multiple times, 178 //TODO Instead of iterating over all entries in the database, which can also pick up the same item multiple times,
186 //we should rather iterate over an index that contains every uid exactly once. The remoteId index would be such an index, 179 //we should rather iterate over an index that contains every uid exactly once. The remoteId index would be such an index,
187 //but we currently fail to iterate over all entries in an index it seems. 180 //but we currently fail to iterate over all entries in an index it seems.
188 // auto remoteIds = synchronizationTransaction.openDatabase("rid.mapping." + bufferType, std::function<void(const Akonadi2::Storage::Error &)>(), true); 181 // auto remoteIds = synchronizationTransaction.openDatabase("rid.mapping." + bufferType, std::function<void(const Akonadi2::Storage::Error &)>(), true);
189 mainDatabase.scan("", [&folderList, this, &transaction, bufferType, &synchronizationTransaction](const QByteArray &key, const QByteArray &) { 182 mainDatabase.scan("", [this, &transaction, bufferType, &synchronizationTransaction, &exists](const QByteArray &key, const QByteArray &) {
190 auto akonadiId = Akonadi2::Storage::uidFromKey(key); 183 auto akonadiId = Akonadi2::Storage::uidFromKey(key);
184 Trace() << "Checking for removal " << key;
191 const auto remoteId = resolveLocalId(bufferType, akonadiId, synchronizationTransaction); 185 const auto remoteId = resolveLocalId(bufferType, akonadiId, synchronizationTransaction);
192 if (!folderList.contains(remoteId)) { 186 if (!remoteId.isEmpty()) {
193 Trace() << "Found a removed entity: " << akonadiId; 187 if (!exists(remoteId.toLatin1())) {
194 deleteEntity(akonadiId, Akonadi2::Storage::maxRevision(transaction), bufferType, [this](const QByteArray &buffer) { 188 Trace() << "Found a removed entity: " << akonadiId;
195 enqueueCommand(mSynchronizerQueue, Akonadi2::Commands::DeleteEntityCommand, buffer); 189 deleteEntity(akonadiId, Akonadi2::Storage::maxRevision(transaction), bufferType, [this](const QByteArray &buffer) {
196 }); 190 enqueueCommand(mSynchronizerQueue, Akonadi2::Commands::DeleteEntityCommand, buffer);
191 });
192 }
197 } 193 }
198 return true; 194 return true;
199 }, 195 },
200 [](const Akonadi2::Storage::Error &error) { 196 [](const Akonadi2::Storage::Error &error) {
201 }); 197 });
202 198
199}
200
201void MaildirResource::createOrModify(Akonadi2::Storage::Transaction &transaction, Akonadi2::Storage::Transaction &synchronizationTransaction, DomainTypeAdaptorFactoryInterface &adaptorFactory, const QByteArray &bufferType, const QByteArray &remoteId, const Akonadi2::ApplicationDomain::ApplicationDomainType &entity)
202{
203 auto mainDatabase = transaction.openDatabase(bufferType + ".main");
204 const auto akonadiId = resolveRemoteId(bufferType, remoteId, synchronizationTransaction).toLatin1();
205 const auto found = mainDatabase.contains(akonadiId);
206 if (!found) {
207 Trace() << "Found a new entity: " << remoteId;
208 createEntity(akonadiId, bufferType, entity, adaptorFactory, [this](const QByteArray &buffer) {
209 enqueueCommand(mSynchronizerQueue, Akonadi2::Commands::CreateEntityCommand, buffer);
210 });
211 } else { //modification
212 if (auto current = getLatest(mainDatabase, akonadiId, adaptorFactory)) {
213 bool changed = false;
214 for (const auto &property : entity.changedProperties()) {
215 if (entity.getProperty(property) != current->getProperty(property)) {
216 Trace() << "Property changed " << akonadiId << property;
217 changed = true;
218 }
219 }
220 if (changed) {
221 Trace() << "Found a modified entity: " << remoteId;
222 modifyEntity(akonadiId, Akonadi2::Storage::maxRevision(transaction), bufferType, entity, adaptorFactory, [this](const QByteArray &buffer) {
223 enqueueCommand(mSynchronizerQueue, Akonadi2::Commands::ModifyEntityCommand, buffer);
224 });
225 }
226 } else {
227 Warning() << "Failed to get current entity";
228 }
229 }
230}
231
232void MaildirResource::synchronizeFolders(Akonadi2::Storage::Transaction &transaction)
233{
234 const QByteArray bufferType = ENTITY_TYPE_FOLDER;
235 QStringList folderList = listAvailableFolders();
236 Trace() << "Found folders " << folderList;
237
238 Akonadi2::Storage store(Akonadi2::storageLocation(), mResourceInstanceIdentifier + ".synchronization", Akonadi2::Storage::ReadWrite);
239 auto synchronizationTransaction = store.createTransaction(Akonadi2::Storage::ReadWrite);
240 auto mainDatabase = transaction.openDatabase(bufferType + ".main");
241 scanForRemovals(transaction, synchronizationTransaction, bufferType, [&folderList](const QByteArray &remoteId) -> bool {
242 return folderList.contains(remoteId);
243 });
244
203 for (const auto folderPath : folderList) { 245 for (const auto folderPath : folderList) {
204 const auto remoteId = folderPath.toUtf8(); 246 const auto remoteId = folderPath.toUtf8();
205 Trace() << "Processing folder " << remoteId; 247 Trace() << "Processing folder " << remoteId;
206 const auto akonadiId = resolveRemoteId(bufferType, remoteId, synchronizationTransaction).toLatin1();
207 const auto found = mainDatabase.contains(akonadiId);
208
209 KPIM::Maildir md(folderPath, folderPath == mMaildirPath); 248 KPIM::Maildir md(folderPath, folderPath == mMaildirPath);
210 249
211 Akonadi2::ApplicationDomain::Folder folder; 250 Akonadi2::ApplicationDomain::Folder folder;
@@ -214,31 +253,7 @@ void MaildirResource::synchronizeFolders(Akonadi2::Storage::Transaction &transac
214 if (!md.isRoot()) { 253 if (!md.isRoot()) {
215 folder.setProperty("parent", resolveRemoteId(ENTITY_TYPE_FOLDER, md.parent().path(), synchronizationTransaction).toLatin1()); 254 folder.setProperty("parent", resolveRemoteId(ENTITY_TYPE_FOLDER, md.parent().path(), synchronizationTransaction).toLatin1());
216 } 255 }
217 256 createOrModify(transaction, synchronizationTransaction, *mFolderAdaptorFactory, bufferType, remoteId, folder);
218 if (!found) {
219 Trace() << "Found a new entity: " << remoteId;
220 createEntity(akonadiId, bufferType, folder, *mFolderAdaptorFactory, [this](const QByteArray &buffer) {
221 enqueueCommand(mSynchronizerQueue, Akonadi2::Commands::CreateEntityCommand, buffer);
222 });
223 } else { //modification
224 if (auto current = getLatest(mainDatabase, akonadiId, *mFolderAdaptorFactory)) {
225 bool changed = false;
226 for (const auto &property : folder.changedProperties()) {
227 if (folder.getProperty(property) != current->getProperty(property)) {
228 Trace() << "Property changed " << akonadiId << property;
229 changed = true;
230 }
231 }
232 if (changed) {
233 Trace() << "Found a modified entity: " << remoteId;
234 modifyEntity(akonadiId, Akonadi2::Storage::maxRevision(transaction), bufferType, folder, *mFolderAdaptorFactory, [this](const QByteArray &buffer) {
235 enqueueCommand(mSynchronizerQueue, Akonadi2::Commands::ModifyEntityCommand, buffer);
236 });
237 }
238 } else {
239 Warning() << "Failed to get current entity";
240 }
241 }
242 } 257 }
243} 258}
244 259
@@ -262,53 +277,36 @@ void MaildirResource::synchronizeMails(Akonadi2::Storage::Transaction &transacti
262 Akonadi2::Storage store(Akonadi2::storageLocation(), mResourceInstanceIdentifier + ".synchronization", Akonadi2::Storage::ReadWrite); 277 Akonadi2::Storage store(Akonadi2::storageLocation(), mResourceInstanceIdentifier + ".synchronization", Akonadi2::Storage::ReadWrite);
263 auto synchronizationTransaction = store.createTransaction(Akonadi2::Storage::ReadWrite); 278 auto synchronizationTransaction = store.createTransaction(Akonadi2::Storage::ReadWrite);
264 279
280 scanForRemovals(transaction, synchronizationTransaction, bufferType, [&listingPath](const QByteArray &remoteId) -> bool {
281 return QFile(listingPath + QDir::separator() + remoteId).exists();
282 });
283
265 while (entryIterator->hasNext()) { 284 while (entryIterator->hasNext()) {
266 QString filePath = entryIterator->next(); 285 QString filePath = entryIterator->next();
267 QString fileName = entryIterator->fileName(); 286 QString fileName = entryIterator->fileName();
268
269 const auto remoteId = fileName.toUtf8(); 287 const auto remoteId = fileName.toUtf8();
270 auto akonadiId = resolveRemoteId(bufferType, remoteId, synchronizationTransaction); 288
271 289 KMime::Message *msg = new KMime::Message;
272 bool found = false; 290 auto filepath = listingPath + QDir::separator() + fileName;
273 transaction.openDatabase(bufferType + ".main").scan(akonadiId.toUtf8(), [&found](const QByteArray &, const QByteArray &) -> bool { 291 msg->setHead(KMime::CRLFtoLF(maildir.readEntryHeadersFromFile(filepath)));
274 found = true; 292 msg->parse();
275 return false; 293
276 }, [this](const Akonadi2::Storage::Error &error) { 294 const auto flags = maildir.readEntryFlags(fileName);
277 }, true); 295
278 296 Trace() << "Found a mail " << filePath << fileName << msg->subject(true)->asUnicodeString();
279 if (!found) { //A new entity 297
280 KMime::Message *msg = new KMime::Message; 298 Akonadi2::ApplicationDomain::Mail mail;
281 auto filepath = listingPath + QDir::separator() + fileName; 299 mail.setProperty("subject", msg->subject(true)->asUnicodeString());
282 msg->setHead(KMime::CRLFtoLF(maildir.readEntryHeadersFromFile(filepath))); 300 mail.setProperty("sender", msg->from(true)->asUnicodeString());
283 msg->parse(); 301 mail.setProperty("senderName", msg->from(true)->asUnicodeString());
284 302 mail.setProperty("date", msg->date(true)->dateTime().toString());
285 const auto flags = maildir.readEntryFlags(fileName); 303 mail.setProperty("folder", resolveRemoteId(ENTITY_TYPE_FOLDER, path, synchronizationTransaction));
286 304 mail.setProperty("mimeMessage", filepath);
287 Trace() << "Found a mail " << filePath << fileName << msg->subject(true)->asUnicodeString(); 305 mail.setProperty("unread", !flags.testFlag(KPIM::Maildir::Seen));
288 306 mail.setProperty("important", flags.testFlag(KPIM::Maildir::Flagged));
289 Akonadi2::ApplicationDomain::Mail mail; 307
290 mail.setProperty("subject", msg->subject(true)->asUnicodeString()); 308 createOrModify(transaction, synchronizationTransaction, *mMailAdaptorFactory, bufferType, remoteId, mail);
291 mail.setProperty("sender", msg->from(true)->asUnicodeString());
292 mail.setProperty("senderName", msg->from(true)->asUnicodeString());
293 mail.setProperty("date", msg->date(true)->dateTime().toString());
294 mail.setProperty("folder", resolveRemoteId(ENTITY_TYPE_FOLDER, path, synchronizationTransaction));
295 mail.setProperty("mimeMessage", filepath);
296 mail.setProperty("unread", !flags.testFlag(KPIM::Maildir::Seen));
297 mail.setProperty("important", flags.testFlag(KPIM::Maildir::Flagged));
298
299 flatbuffers::FlatBufferBuilder entityFbb;
300 mMailAdaptorFactory->createBuffer(mail, entityFbb);
301
302 Trace() << "Found a new entity: " << remoteId;
303 createEntity(akonadiId.toLatin1(), bufferType, mail, *mMailAdaptorFactory, [this](const QByteArray &buffer) {
304 enqueueCommand(mSynchronizerQueue, Akonadi2::Commands::CreateEntityCommand, buffer);
305 });
306 } else { //modification
307 Trace() << "Found a modified entity: " << remoteId;
308 //TODO diff and create modification if necessary
309 }
310 } 309 }
311 //TODO find items to remove
312} 310}
313 311
314KAsync::Job<void> MaildirResource::synchronizeWithSource() 312KAsync::Job<void> MaildirResource::synchronizeWithSource()
diff --git a/examples/maildirresource/maildirresource.h b/examples/maildirresource/maildirresource.h
index e1eecc1..eec1e97 100644
--- a/examples/maildirresource/maildirresource.h
+++ b/examples/maildirresource/maildirresource.h
@@ -39,8 +39,36 @@ public:
39 static void removeFromDisk(const QByteArray &instanceIdentifier); 39 static void removeFromDisk(const QByteArray &instanceIdentifier);
40private: 40private:
41 KAsync::Job<void> replay(const QByteArray &type, const QByteArray &key, const QByteArray &value) Q_DECL_OVERRIDE; 41 KAsync::Job<void> replay(const QByteArray &type, const QByteArray &key, const QByteArray &value) Q_DECL_OVERRIDE;
42
43 /**
44 * Tries to find a local id for the remote id, and creates a new local id otherwise.
45 *
46 * The new local id is recorded in the local to remote id mapping.
47 */
42 QString resolveRemoteId(const QByteArray &type, const QString &remoteId, Akonadi2::Storage::Transaction &transaction); 48 QString resolveRemoteId(const QByteArray &type, const QString &remoteId, Akonadi2::Storage::Transaction &transaction);
49
50 /**
51 * Tries to find a remote id for a local id.
52 *
53 * This can fail if the entity hasn't been written back to the server yet.
54 */
43 QString resolveLocalId(const QByteArray &bufferType, const QByteArray &localId, Akonadi2::Storage::Transaction &transaction); 55 QString resolveLocalId(const QByteArray &bufferType, const QByteArray &localId, Akonadi2::Storage::Transaction &transaction);
56
57 /**
58 * An algorithm to remove entities that are no longer existing.
59 *
60 * This algorithm calls @param exists for every entity of type @param type, with its remoteId. For every entity where @param exists returns false,
61 * an entity delete command is enqueued.
62 */
63 void scanForRemovals(Akonadi2::Storage::Transaction &transaction, Akonadi2::Storage::Transaction &synchronizationTransaction, const QByteArray &bufferType, std::function<bool(const QByteArray &remoteId)> exists);
64
65 /**
66 * An algorithm to create or modify the entity.
67 *
68 * Depending on whether the entity is locally available, or has changed.
69 */
70 void createOrModify(Akonadi2::Storage::Transaction &transaction, Akonadi2::Storage::Transaction &synchronizationTransaction, DomainTypeAdaptorFactoryInterface &adaptorFactory, const QByteArray &bufferType, const QByteArray &remoteId, const Akonadi2::ApplicationDomain::ApplicationDomainType &entity);
71
44 void synchronizeFolders(Akonadi2::Storage::Transaction &transaction); 72 void synchronizeFolders(Akonadi2::Storage::Transaction &transaction);
45 void synchronizeMails(Akonadi2::Storage::Transaction &transaction, const QString &folder); 73 void synchronizeMails(Akonadi2::Storage::Transaction &transaction, const QString &folder);
46 QStringList listAvailableFolders(); 74 QStringList listAvailableFolders();
diff --git a/tests/maildirresourcetest.cpp b/tests/maildirresourcetest.cpp
index e6b6849..a4856ce 100644
--- a/tests/maildirresourcetest.cpp
+++ b/tests/maildirresourcetest.cpp
@@ -149,6 +149,26 @@ private Q_SLOTS:
149 QVERIFY(mailModel->rowCount(QModelIndex()) >= 1); 149 QVERIFY(mailModel->rowCount(QModelIndex()) >= 1);
150 } 150 }
151 151
152 void testMailContent()
153 {
154 Akonadi2::Query query;
155 query.resources << "org.kde.maildir.instance1";
156 query.requestedProperties << "folder" << "subject" << "mimeMessage";
157 query.syncOnDemand = true;
158 query.processAll = true;
159
160 //Ensure all local data is processed
161 Akonadi2::Store::synchronize(query).exec().waitForFinished();
162
163 auto mailModel = Akonadi2::Store::loadModel<Akonadi2::ApplicationDomain::Mail>(query);
164 QTRY_VERIFY(mailModel->data(QModelIndex(), Akonadi2::Store::ChildrenFetchedRole).toBool());
165 QVERIFY(mailModel->rowCount(QModelIndex()) >= 1);
166 auto mail = mailModel->index(0, 0, QModelIndex()).data(Akonadi2::Store::DomainObjectRole).value<Akonadi2::ApplicationDomain::Mail::Ptr>();
167 QVERIFY(!mail->getProperty("subject").toString().isEmpty());
168 QVERIFY(!mail->getProperty("mimeMessage").toString().isEmpty());
169 }
170
171
152 void testSyncFolderMove() 172 void testSyncFolderMove()
153 { 173 {
154 Akonadi2::Query query; 174 Akonadi2::Query query;
@@ -173,6 +193,29 @@ private Q_SLOTS:
173 QCOMPARE(model->match(model->index(0, 0, QModelIndex()), Qt::DisplayRole, QStringLiteral("newbox"), 1).size(), 1); 193 QCOMPARE(model->match(model->index(0, 0, QModelIndex()), Qt::DisplayRole, QStringLiteral("newbox"), 1).size(), 1);
174 } 194 }
175 195
196 void testSyncMailMove()
197 {
198 Akonadi2::Query query;
199 query.resources << "org.kde.maildir.instance1";
200 query.syncOnDemand = true;
201 query.processAll = true;
202 query.requestedProperties << "folder" << "summary";
203
204 //Ensure all local data is processed
205 Akonadi2::Store::synchronize(query).exec().waitForFinished();
206
207 auto targetPath = tempDir.path() + QDir::separator() + "maildir1/cur/1365777830.R28.localhost.localdomain:2,S";
208 QFile file(targetPath);
209 QVERIFY(file.remove());
210
211 //Ensure all local data is processed
212 Akonadi2::Store::synchronize(query).exec().waitForFinished();
213
214 auto mailModel = Akonadi2::Store::loadModel<Akonadi2::ApplicationDomain::Mail>(query);
215 QTRY_VERIFY(mailModel->data(QModelIndex(), Akonadi2::Store::ChildrenFetchedRole).toBool());
216 QCOMPARE(mailModel->rowCount(QModelIndex()), 1);
217 }
218
176}; 219};
177 220
178QTEST_MAIN(MaildirResourceTest) 221QTEST_MAIN(MaildirResourceTest)