Marble

BookmarkSyncManager.cpp
1// SPDX-License-Identifier: LGPL-2.1-or-later
2//
3// SPDX-FileCopyrightText: 2013 Utku Aydın <utkuaydin34@gmail.com>
4//
5
6#include "BookmarkSyncManager.h"
7
8#include "BookmarkManager.h"
9#include "CloudSyncManager.h"
10#include "GeoDataCoordinates.h"
11#include "GeoDataDocument.h"
12#include "GeoDataFolder.h"
13#include "GeoDataLookAt.h"
14#include "GeoDataParser.h"
15#include "GeoWriter.h"
16#include "MarbleDebug.h"
17#include "MarbleDirs.h"
18#include "MarbleModel.h"
19#include "OwncloudSyncBackend.h"
20
21#include <QBuffer>
22#include <QFile>
23#include <QJsonDocument>
24#include <QJsonObject>
25#include <QNetworkAccessManager>
26#include <QNetworkReply>
27#include <QTimer>
28
29namespace Marble
30{
31
32class DiffItem
33{
34public:
35 enum Action {
36 NoAction,
37 Created,
38 Changed,
39 Deleted
40 };
41
42 enum Status {
43 Source,
44 Destination
45 };
46
47 QString m_path;
48 Action m_action;
49 Status m_origin;
50 GeoDataPlacemark m_placemarkA;
51 GeoDataPlacemark m_placemarkB;
52};
53
54class Q_DECL_HIDDEN BookmarkSyncManager::Private
55{
56public:
57 Private(BookmarkSyncManager *parent, CloudSyncManager *cloudSyncManager);
58
59 BookmarkSyncManager *const m_q;
60 CloudSyncManager *const m_cloudSyncManager;
61
62 QNetworkAccessManager m_network;
63 QString m_uploadEndpoint;
64 QString m_downloadEndpoint;
65 QString m_timestampEndpoint;
66
67 QNetworkReply *m_uploadReply;
68 QNetworkReply *m_downloadReply;
69 QNetworkReply *m_timestampReply;
70
71 QString m_cloudTimestamp;
72
73 QString m_cachePath;
74 QString m_localBookmarksPath;
75 QString m_bookmarksTimestamp;
76
77 QList<DiffItem> m_diffA;
78 QList<DiffItem> m_diffB;
79 QList<DiffItem> m_merged;
80 DiffItem m_conflictItem;
81
82 BookmarkManager *m_bookmarkManager = nullptr;
83 QTimer m_syncTimer;
84 bool m_bookmarkSyncEnabled;
85
86 /**
87 * Returns an API endpoint
88 * @param endpoint Endpoint itself without server info
89 * @return Complete API URL as QUrl
90 */
91 QUrl endpointUrl(const QString &endpoint) const;
92
93 /**
94 * Uploads local bookmarks.kml to cloud.
95 */
96 void uploadBookmarks();
97
98 /**
99 * Downloads bookmarks.kml from cloud.
100 */
101 void downloadBookmarks();
102
103 /**
104 * Gets cloud bookmarks.kml's timestamp from cloud.
105 */
106 void downloadTimestamp();
107
108 /**
109 * Compares cloud bookmarks.kml's timestamp to last synced bookmarks.kml's timestamp.
110 * @return true if cloud one is different from last synced one.
111 */
112 bool cloudBookmarksModified(const QString &cloudTimestamp) const;
113
114 /**
115 * Removes all KMLs in the cache except the
116 * one with youngest timestamp.
117 */
118 void clearCache();
119
120 /**
121 * Finds the last synced bookmarks.kml file and returns its path
122 * @return Path of last synced bookmarks.kml file.
123 */
124 QString lastSyncedKmlPath() const;
125
126 /**
127 * Gets all placemarks in a document as DiffItems, compares them to another document and puts the result in a list.
128 * @param document The document whose placemarks will be compared to another document's placemarks.
129 * @param other The document whose placemarks will be compared to the first document's placemarks.
130 * @param diffDirection Direction of comparison, e.g. must be DiffItem::Destination if direction is source to destination.
131 * @return A list of DiffItems
132 */
133 QList<DiffItem> getPlacemarks(GeoDataDocument *document, GeoDataDocument *other, DiffItem::Status diffDirection);
134
135 /**
136 * Gets all placemarks in a document as DiffItems, compares them to another document and puts the result in a list.
137 * @param folder The folder whose placemarks will be compared to another document's placemarks.
138 * @param path Path of the folder.
139 * @param other The document whose placemarks will be compared to the first document's placemarks.
140 * @param diffDirection Direction of comparison, e.g. must be DiffItem::Destination if direction is source to destination.
141 * @return A list of DiffItems
142 */
143 QList<DiffItem> getPlacemarks(GeoDataFolder *folder, QString &path, GeoDataDocument *other, DiffItem::Status diffDirection);
144
145 /**
146 * Finds the placemark which has the same coordinates with given bookmark
147 * @param container Container of placemarks which will be compared. Can be document or folder.
148 * @param bookmark The bookmark whose counterpart will be searched in the container.
149 * @return Counterpart of the given placemark.
150 */
151 const GeoDataPlacemark *findPlacemark(GeoDataContainer *container, const GeoDataPlacemark &bookmark) const;
152
153 /**
154 * Determines the status (created, deleted, changed or unchanged) of given DiffItem
155 * by comparing the item's placemark with placemarks of given GeoDataDocument.
156 * @param item The item whose status will be determined.
157 * @param document The document whose placemarks will be used to determine DiffItem's status.
158 */
159 void determineDiffStatus(DiffItem &item, GeoDataDocument *document) const;
160
161 /**
162 * Finds differences between two bookmark files.
163 * @param sourcePath Source bookmark
164 * @param destinationPath Destination bookmark
165 * @return A list of differences
166 */
167 QList<DiffItem> diff(QString &sourcePath, QString &destinationPath);
168 QList<DiffItem> diff(QString &sourcePath, QIODevice *destination);
169 QList<DiffItem> diff(QIODevice *source, QString &destinationPath);
170 QList<DiffItem> diff(QIODevice *source, QIODevice *destination);
171
172 /**
173 * Merges two diff lists.
174 * @param diffListA First diff list.
175 * @param diffListB Second diff list.
176 * @return Merged DiffItems.
177 */
178 void merge();
179
180 /**
181 * Creates GeoDataFolders using strings in path list.
182 * @param container Container which created GeoDataFolder will be attached to.
183 * @param pathList Names of folders. Note that each item will be the child of the previous one.
184 * @return A pointer to created folder.
185 */
186 GeoDataFolder *createFolders(GeoDataContainer *container, QStringList &pathList);
187
188 /**
189 * Creates a GeoDataDocument using a list of DiffItems.
190 * @param mergedList DiffItems which will be used as placemarks.
191 * @return A pointer to created document.
192 */
193 GeoDataDocument *constructDocument(const QList<DiffItem> &mergedList);
194
195 void saveDownloadedToCache(const QByteArray &kml);
196
197 void parseTimestamp();
198 void copyLocalToCache();
199
200 void continueSynchronization();
201 void completeSynchronization();
202 void completeMerge();
203 void completeUpload();
204};
205
206BookmarkSyncManager::Private::Private(BookmarkSyncManager *parent, CloudSyncManager *cloudSyncManager)
207 : m_q(parent)
208 , m_cloudSyncManager(cloudSyncManager)
209 , m_bookmarkManager(nullptr)
210 , m_bookmarkSyncEnabled(false)
211{
212 m_cachePath = MarbleDirs::localPath() + QLatin1StringView("/cloudsync/cache/bookmarks");
213 m_localBookmarksPath = MarbleDirs::localPath() + QLatin1StringView("/bookmarks/bookmarks.kml");
214 m_downloadEndpoint = QStringLiteral("bookmarks/kml");
215 m_uploadEndpoint = QStringLiteral("bookmarks/update");
216 m_timestampEndpoint = QStringLiteral("bookmarks/timestamp");
217}
218
219BookmarkSyncManager::BookmarkSyncManager(CloudSyncManager *cloudSyncManager)
220 : QObject()
221 , d(new Private(this, cloudSyncManager))
222{
223 d->m_syncTimer.setInterval(60 * 60 * 1000); // 1 hour. TODO: Make this configurable.
224 connect(&d->m_syncTimer, &QTimer::timeout, this, &BookmarkSyncManager::startBookmarkSync);
225}
226
227BookmarkSyncManager::~BookmarkSyncManager()
228{
229 delete d;
230}
231
232QDateTime BookmarkSyncManager::lastSync() const
233{
234 const QString last = d->lastSyncedKmlPath();
235 if (last.isEmpty())
236 return {};
237 return QFileInfo(last).metadataChangeTime();
238}
239
240bool BookmarkSyncManager::isBookmarkSyncEnabled() const
241{
242 return d->m_bookmarkSyncEnabled;
243}
244
245void BookmarkSyncManager::setBookmarkSyncEnabled(bool enabled)
246{
247 bool const old_state = isBookmarkSyncEnabled();
248 d->m_bookmarkSyncEnabled = enabled;
249 if (old_state != isBookmarkSyncEnabled()) {
250 Q_EMIT bookmarkSyncEnabledChanged(d->m_bookmarkSyncEnabled);
251 if (isBookmarkSyncEnabled()) {
252 startBookmarkSync();
253 }
254 }
255}
256
257void BookmarkSyncManager::setBookmarkManager(BookmarkManager *manager)
258{
259 d->m_bookmarkManager = manager;
260 connect(manager, SIGNAL(bookmarksChanged()), this, SLOT(startBookmarkSync()));
261 startBookmarkSync();
262}
263
264void BookmarkSyncManager::startBookmarkSync()
265{
266 if (!d->m_cloudSyncManager->isSyncEnabled() || !isBookmarkSyncEnabled()) {
267 return;
268 }
269
270 d->m_syncTimer.start();
271 d->downloadTimestamp();
272}
273
274QUrl BookmarkSyncManager::Private::endpointUrl(const QString &endpoint) const
275{
276 return QUrl(m_cloudSyncManager->apiUrl().toString() + QLatin1Char('/') + endpoint);
277}
278
279void BookmarkSyncManager::Private::uploadBookmarks()
280{
281 QByteArray data;
282 QByteArray lineBreak = "\r\n";
283 QString word = QStringLiteral("----MarbleCloudBoundary");
284 QString boundary = QStringLiteral("--%0").arg(word);
285 QNetworkRequest request(endpointUrl(m_uploadEndpoint));
286 request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("multipart/form-data; boundary=%0").arg(word));
287
288 data.append(QString(boundary + lineBreak).toUtf8());
289 data.append(R"(Content-Disposition: form-data; name="bookmarks"; filename="bookmarks.kml")" + lineBreak);
290 data.append("Content-Type: application/vnd.google-earth.kml+xml" + lineBreak + lineBreak);
291
292 QFile bookmarksFile(m_localBookmarksPath);
293 if (!bookmarksFile.open(QFile::ReadOnly)) {
294 mDebug() << "Failed to open file" << bookmarksFile.fileName() << ". It is either missing or not readable.";
295 return;
296 }
297
298 QByteArray kmlContent = bookmarksFile.readAll();
299 data.append(kmlContent + lineBreak + lineBreak);
300 data.append(QString(boundary).toUtf8());
301 bookmarksFile.close();
302
303 m_uploadReply = m_network.post(request, data);
304 connect(m_uploadReply, SIGNAL(uploadProgress(qint64, qint64)), m_q, SIGNAL(uploadProgress(qint64, qint64)));
305 connect(m_uploadReply, SIGNAL(finished()), m_q, SLOT(completeUpload()));
306}
307
308void BookmarkSyncManager::Private::downloadBookmarks()
309{
310 QNetworkRequest request(endpointUrl(m_downloadEndpoint));
311 m_downloadReply = m_network.get(request);
312 connect(m_downloadReply, SIGNAL(finished()), m_q, SLOT(completeSynchronization()));
313 connect(m_downloadReply, SIGNAL(downloadProgress(qint64, qint64)), m_q, SIGNAL(downloadProgress(qint64, qint64)));
314}
315
316void BookmarkSyncManager::Private::downloadTimestamp()
317{
318 mDebug() << "Determining remote bookmark state.";
319 m_timestampReply = m_network.get(QNetworkRequest(endpointUrl(m_timestampEndpoint)));
320 connect(m_timestampReply, SIGNAL(finished()), m_q, SLOT(parseTimestamp()));
321}
322
323bool BookmarkSyncManager::Private::cloudBookmarksModified(const QString &cloudTimestamp) const
324{
325 QStringList entryList = QDir(m_cachePath)
326 .entryList(
327 // TODO: replace with regex filter that only
328 // allows timestamp filenames
329 QStringList() << QStringLiteral("*.kml"),
331 QDir::Name);
332 if (!entryList.isEmpty()) {
333 QString lastSynced = entryList.last();
334 lastSynced.chop(4);
335 return cloudTimestamp != lastSynced;
336 } else {
337 return true; // That will let cloud one get downloaded.
338 }
339}
340
341void BookmarkSyncManager::Private::clearCache()
342{
343 QDir cacheDir(m_cachePath);
344 QFileInfoList fileInfoList = cacheDir.entryInfoList(QStringList() << QStringLiteral("*.kml"), QDir::NoFilter, QDir::Name);
345 if (!fileInfoList.isEmpty()) {
346 for (const QFileInfo &fileInfo : std::as_const(fileInfoList)) {
347 QFile file(fileInfo.absoluteFilePath());
348 bool removed = file.remove();
349 if (!removed) {
350 mDebug() << "Could not delete" << file.fileName() << "Make sure you have sufficient permissions.";
351 }
352 }
353 }
354}
355
356QString BookmarkSyncManager::Private::lastSyncedKmlPath() const
357{
358 QDir cacheDir(m_cachePath);
359 QFileInfoList fileInfoList = cacheDir.entryInfoList(QStringList() << QStringLiteral("*.kml"), QDir::NoFilter, QDir::Name);
360 if (!fileInfoList.isEmpty()) {
361 return fileInfoList.last().absoluteFilePath();
362 } else {
363 return {};
364 }
365}
366
367QList<DiffItem> BookmarkSyncManager::Private::getPlacemarks(GeoDataDocument *document, GeoDataDocument *other, DiffItem::Status diffDirection)
368{
369 QList<DiffItem> diffItems;
370 const auto folders = document->folderList();
371 for (GeoDataFolder *folder : folders) {
372 QString path = QStringLiteral("/%0").arg(folder->name());
373 diffItems.append(getPlacemarks(folder, path, other, diffDirection));
374 }
375
376 return diffItems;
377}
378
379QList<DiffItem> BookmarkSyncManager::Private::getPlacemarks(GeoDataFolder *folder, QString &path, GeoDataDocument *other, DiffItem::Status diffDirection)
380{
381 QList<DiffItem> diffItems;
382 const auto folders = folder->folderList();
383 for (GeoDataFolder *subFolder : folders) {
384 QString newPath = QStringLiteral("%0/%1").arg(path, subFolder->name());
385 diffItems.append(getPlacemarks(subFolder, newPath, other, diffDirection));
386 }
387
388 const auto placemarks = folder->placemarkList();
389 for (GeoDataPlacemark *placemark : placemarks) {
390 DiffItem diffItem;
391 diffItem.m_path = path;
392 diffItem.m_placemarkA = *placemark;
393 switch (diffDirection) {
394 case DiffItem::Source:
395 diffItem.m_origin = DiffItem::Destination;
396 break;
397 case DiffItem::Destination:
398 diffItem.m_origin = DiffItem::Source;
399 break;
400 default:
401 break;
402 }
403
404 determineDiffStatus(diffItem, other);
405
406 if (!(diffItem.m_action == DiffItem::NoAction && diffItem.m_origin == DiffItem::Destination)
407 && !(diffItem.m_action == DiffItem::Changed && diffItem.m_origin == DiffItem::Source)) {
408 diffItems.append(diffItem);
409 }
410 }
411
412 return diffItems;
413}
414
415const GeoDataPlacemark *BookmarkSyncManager::Private::findPlacemark(GeoDataContainer *container, const GeoDataPlacemark &bookmark) const
416{
417 const auto placemarks = container->placemarkList();
418 for (GeoDataPlacemark *placemark : placemarks) {
419 if (EARTH_RADIUS * placemark->coordinate().sphericalDistanceTo(bookmark.coordinate()) <= 1) {
420 return placemark;
421 }
422 }
423
424 const auto folders = container->folderList();
425 for (GeoDataFolder *folder : folders) {
426 const GeoDataPlacemark *placemark = findPlacemark(folder, bookmark);
427 if (placemark) {
428 return placemark;
429 }
430 }
431
432 return nullptr;
433}
434
435void BookmarkSyncManager::Private::determineDiffStatus(DiffItem &item, GeoDataDocument *document) const
436{
437 const GeoDataPlacemark *match = findPlacemark(document, item.m_placemarkA);
438
439 if (match != nullptr) {
440 item.m_placemarkB = *match;
441 bool nameChanged = item.m_placemarkA.name() != item.m_placemarkB.name();
442 bool descChanged = item.m_placemarkA.description() != item.m_placemarkB.description();
443 bool lookAtChanged = item.m_placemarkA.lookAt()->latitude() != item.m_placemarkB.lookAt()->latitude()
444 || item.m_placemarkA.lookAt()->longitude() != item.m_placemarkB.lookAt()->longitude()
445 || item.m_placemarkA.lookAt()->altitude() != item.m_placemarkB.lookAt()->altitude()
446 || item.m_placemarkA.lookAt()->range() != item.m_placemarkB.lookAt()->range();
447 if (nameChanged || descChanged || lookAtChanged) {
448 item.m_action = DiffItem::Changed;
449 } else {
450 item.m_action = DiffItem::NoAction;
451 }
452 } else {
453 switch (item.m_origin) {
454 case DiffItem::Source:
455 item.m_action = DiffItem::Deleted;
456 item.m_placemarkB = item.m_placemarkA; // for conflict purposes
457 break;
458 case DiffItem::Destination:
459 item.m_action = DiffItem::Created;
460 break;
461 }
462 }
463}
464
465QList<DiffItem> BookmarkSyncManager::Private::diff(QString &sourcePath, QString &destinationPath)
466{
467 QFile fileB(destinationPath);
468 if (!fileB.open(QFile::ReadOnly)) {
469 mDebug() << "Could not open file " << fileB.fileName();
470 }
471 return diff(sourcePath, &fileB);
472}
473
474QList<DiffItem> BookmarkSyncManager::Private::diff(QString &sourcePath, QIODevice *fileB)
475{
476 QFile fileA(sourcePath);
477 if (!fileA.open(QFile::ReadOnly)) {
478 mDebug() << "Could not open file " << fileA.fileName();
479 }
480
481 return diff(&fileA, fileB);
482}
483
484QList<DiffItem> BookmarkSyncManager::Private::diff(QIODevice *source, QString &destinationPath)
485{
486 QFile fileB(destinationPath);
487 if (!fileB.open(QFile::ReadOnly)) {
488 mDebug() << "Could not open file " << fileB.fileName();
489 }
490
491 return diff(source, &fileB);
492}
493
494QList<DiffItem> BookmarkSyncManager::Private::diff(QIODevice *fileA, QIODevice *fileB)
495{
496 GeoDataParser parserA(GeoData_KML);
497 parserA.read(fileA);
498 auto documentA = dynamic_cast<GeoDataDocument *>(parserA.releaseDocument());
499
500 GeoDataParser parserB(GeoData_KML);
501 parserB.read(fileB);
502 auto documentB = dynamic_cast<GeoDataDocument *>(parserB.releaseDocument());
503
504 QList<DiffItem> diffItems = getPlacemarks(documentA, documentB, DiffItem::Destination); // Compare old to new
505 diffItems.append(getPlacemarks(documentB, documentA, DiffItem::Source)); // Compare new to old
506
507 // Compare paths
508 for (int i = 0; i < diffItems.count(); i++) {
509 for (int p = i + 1; p < diffItems.count(); p++) {
510 if ((diffItems[i].m_origin == DiffItem::Source) && (diffItems[i].m_action == DiffItem::NoAction)
511 && (EARTH_RADIUS * diffItems[i].m_placemarkA.coordinate().sphericalDistanceTo(diffItems[p].m_placemarkB.coordinate()) <= 1)
512 && (EARTH_RADIUS * diffItems[i].m_placemarkB.coordinate().sphericalDistanceTo(diffItems[p].m_placemarkA.coordinate()) <= 1)
513 && (diffItems[i].m_path != diffItems[p].m_path)) {
514 diffItems[p].m_action = DiffItem::Changed;
515 }
516 }
517 }
518
519 return diffItems;
520}
521
522void BookmarkSyncManager::Private::merge()
523{
524 for (const DiffItem &itemA : std::as_const(m_diffA)) {
525 if (itemA.m_action == DiffItem::NoAction) {
526 bool deleted = false;
527 bool changed = false;
528 DiffItem other;
529
530 for (const DiffItem &itemB : std::as_const(m_diffB)) {
531 if (EARTH_RADIUS * itemA.m_placemarkA.coordinate().sphericalDistanceTo(itemB.m_placemarkA.coordinate()) <= 1) {
532 if (itemB.m_action == DiffItem::Deleted) {
533 deleted = true;
534 } else if (itemB.m_action == DiffItem::Changed) {
535 changed = true;
536 other = itemB;
537 }
538 }
539 }
540 if (changed) {
541 m_merged.append(other);
542 } else if (!deleted) {
543 m_merged.append(itemA);
544 }
545 } else if (itemA.m_action == DiffItem::Created) {
546 m_merged.append(itemA);
547 } else if (itemA.m_action == DiffItem::Changed || itemA.m_action == DiffItem::Deleted) {
548 bool conflict = false;
549 DiffItem other;
550
551 for (const DiffItem &itemB : std::as_const(m_diffB)) {
552 if (EARTH_RADIUS * itemA.m_placemarkB.coordinate().sphericalDistanceTo(itemB.m_placemarkB.coordinate()) <= 1) {
553 if ((itemA.m_action == DiffItem::Changed && (itemB.m_action == DiffItem::Changed || itemB.m_action == DiffItem::Deleted))
554 || (itemA.m_action == DiffItem::Deleted && itemB.m_action == DiffItem::Changed)) {
555 conflict = true;
556 other = itemB;
557 }
558 }
559 }
560
561 if (!conflict && itemA.m_action == DiffItem::Changed) {
562 m_merged.append(itemA);
563 } else if (conflict) {
564 m_conflictItem = other;
565 auto mergeItem = new MergeItem();
566 mergeItem->setPathA(itemA.m_path);
567 mergeItem->setPathB(other.m_path);
568 mergeItem->setPlacemarkA(itemA.m_placemarkA);
569 mergeItem->setPlacemarkB(other.m_placemarkA);
570
571 switch (itemA.m_action) {
572 case DiffItem::Changed:
573 mergeItem->setActionA(MergeItem::Changed);
574 break;
575 case DiffItem::Deleted:
576 mergeItem->setActionA(MergeItem::Deleted);
577 break;
578 default:
579 break;
580 }
581
582 switch (other.m_action) {
583 case DiffItem::Changed:
584 mergeItem->setActionB(MergeItem::Changed);
585 break;
586 case DiffItem::Deleted:
587 mergeItem->setActionB(MergeItem::Deleted);
588 break;
589 default:
590 break;
591 }
592
593 Q_EMIT m_q->mergeConflict(mergeItem);
594 return;
595 }
596 }
597
598 if (!m_diffA.isEmpty()) {
599 m_diffA.removeFirst();
600 }
601 }
602
603 for (const DiffItem &itemB : std::as_const(m_diffB)) {
604 if (itemB.m_action == DiffItem::Created) {
605 m_merged.append(itemB);
606 }
607 }
608
609 completeMerge();
610}
611
612GeoDataFolder *BookmarkSyncManager::Private::createFolders(GeoDataContainer *container, QStringList &pathList)
613{
614 GeoDataFolder *folder = nullptr;
615 if (pathList.count() > 0) {
616 const QString name = pathList.takeFirst();
617
618 const auto otherFolders = container->folderList();
619 for (GeoDataFolder *otherFolder : otherFolders) {
620 if (otherFolder->name() == name) {
621 folder = otherFolder;
622 }
623 }
624
625 if (folder == nullptr) {
626 folder = new GeoDataFolder();
627 folder->setName(name);
628 container->append(folder);
629 }
630
631 if (pathList.isEmpty()) {
632 return folder;
633 }
634 }
635
636 return createFolders(folder, pathList);
637}
638
639GeoDataDocument *BookmarkSyncManager::Private::constructDocument(const QList<DiffItem> &mergedList)
640{
641 auto document = new GeoDataDocument();
642 document->setName(tr("Bookmarks"));
643
644 for (const DiffItem &item : mergedList) {
645 auto placemark = new GeoDataPlacemark(item.m_placemarkA);
646 QStringList splitten = item.m_path.split(QLatin1Char('/'), Qt::SkipEmptyParts);
647 GeoDataFolder *folder = createFolders(document, splitten);
648 folder->append(placemark);
649 }
650
651 return document;
652}
653
654void BookmarkSyncManager::resolveConflict(MergeItem *item)
655{
656 DiffItem diffItem;
657
658 switch (item->resolution()) {
659 case MergeItem::A:
660 if (!d->m_diffA.isEmpty()) {
661 diffItem = d->m_diffA.first();
662 break;
663 }
664 Q_FALLTHROUGH();
665 case MergeItem::B:
666 diffItem = d->m_conflictItem;
667 break;
668 default:
669 return; // It shouldn't happen.
670 }
671
672 if (diffItem.m_action != DiffItem::Deleted) {
673 d->m_merged.append(diffItem);
674 }
675
676 if (!d->m_diffA.isEmpty()) {
677 d->m_diffA.removeFirst();
678 }
679
680 d->merge();
681}
682
683void BookmarkSyncManager::Private::saveDownloadedToCache(const QByteArray &kml)
684{
685 QString localBookmarksDir = m_localBookmarksPath;
686 QDir().mkdir(localBookmarksDir.remove(QStringLiteral("bookmarks.kml")));
687 QFile bookmarksFile(m_localBookmarksPath);
688 if (!bookmarksFile.open(QFile::ReadWrite)) {
689 mDebug() << "Failed to open file" << bookmarksFile.fileName() << ". It is either missing or not readable.";
690 return;
691 }
692
693 bookmarksFile.write(kml);
694 bookmarksFile.close();
695 copyLocalToCache();
696}
697
698void BookmarkSyncManager::Private::parseTimestamp()
699{
700 QJsonDocument jsonDoc = QJsonDocument::fromJson(m_timestampReply->readAll());
701 QJsonValue dataValue = jsonDoc.object().value(QStringLiteral("data"));
702
703 m_cloudTimestamp = dataValue.toString();
704 mDebug() << "Remote bookmark timestamp is " << m_cloudTimestamp;
705 continueSynchronization();
706}
707void BookmarkSyncManager::Private::copyLocalToCache()
708{
709 QDir().mkpath(m_cachePath);
710 clearCache();
711
712 QFile bookmarksFile(m_localBookmarksPath);
713 bookmarksFile.copy(QStringLiteral("%0/%1.kml").arg(m_cachePath, m_cloudTimestamp));
714}
715
716// Bookmark synchronization steps
717void BookmarkSyncManager::Private::continueSynchronization()
718{
719 bool cloudModified = cloudBookmarksModified(m_cloudTimestamp);
720 if (cloudModified) {
721 downloadBookmarks();
722 } else {
723 QString lastSyncedPath = lastSyncedKmlPath();
724 if (lastSyncedPath.isEmpty()) {
725 mDebug() << "Never synced. Uploading bookmarks.";
726 uploadBookmarks();
727 } else {
728 QList<DiffItem> diffList = diff(lastSyncedPath, m_localBookmarksPath);
729 bool localModified = false;
730 for (const DiffItem &item : std::as_const(diffList)) {
731 if (item.m_action != DiffItem::NoAction) {
732 localModified = true;
733 }
734 }
735
736 if (localModified) {
737 mDebug() << "Local modifications, uploading.";
738 uploadBookmarks();
739 }
740 }
741 }
742}
743
744void BookmarkSyncManager::Private::completeSynchronization()
745{
746 mDebug() << "Merging remote and local bookmark file";
747 QString lastSyncedPath = lastSyncedKmlPath();
748 QFile localBookmarksFile(m_localBookmarksPath);
749 QByteArray result = m_downloadReply->readAll();
750 QBuffer buffer(&result);
751 buffer.open(QIODevice::ReadOnly);
752
753 if (lastSyncedPath.isEmpty()) {
754 if (localBookmarksFile.exists()) {
755 mDebug() << "Conflict between remote bookmarks and local ones";
756 m_diffA = diff(&buffer, m_localBookmarksPath);
757 m_diffB = diff(m_localBookmarksPath, &buffer);
758 } else {
759 saveDownloadedToCache(result);
760 return;
761 }
762 } else {
763 m_diffA = diff(lastSyncedPath, m_localBookmarksPath);
764 m_diffB = diff(lastSyncedPath, &buffer);
765 }
766
767 m_merged.clear();
768 merge();
769}
770
771void BookmarkSyncManager::Private::completeMerge()
772{
773 QFile localBookmarksFile(m_localBookmarksPath);
774 GeoDataDocument *doc = constructDocument(m_merged);
775 GeoWriter writer;
776 localBookmarksFile.remove();
777 localBookmarksFile.open(QFile::ReadWrite);
778 writer.write(&localBookmarksFile, doc);
779 localBookmarksFile.close();
780 uploadBookmarks();
781}
782
783void BookmarkSyncManager::Private::completeUpload()
784{
785 QJsonDocument jsonDoc = QJsonDocument::fromJson(m_uploadReply->readAll());
786 QJsonValue dataValue = jsonDoc.object().value(QStringLiteral("data"));
787
788 m_cloudTimestamp = dataValue.toString();
789 mDebug() << "Uploaded bookmarks to remote server. Timestamp is " << m_cloudTimestamp;
790 copyLocalToCache();
791 Q_EMIT m_q->syncComplete();
792}
793
794}
795
796#include "moc_BookmarkSyncManager.cpp"
This file contains the headers for MarbleModel.
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
QString path(const QString &relativePath)
QStringView merge(QStringView lhs, QStringView rhs)
QString name(StandardAction id)
Binds a QML item to a specific geodetic location in screen coordinates.
QByteArray & append(QByteArrayView data)
QStringList entryList(Filters filters, SortFlags sort) const const
bool mkdir(const QString &dirName) const const
bool mkpath(const QString &dirPath) const const
QDateTime metadataChangeTime() const const
virtual bool open(QIODeviceBase::OpenMode mode)
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
QJsonObject object() const const
QJsonValue value(QLatin1StringView key) const const
QString toString() const const
void append(QList< T > &&value)
qsizetype count() const const
bool isEmpty() const const
T & last()
value_type takeFirst()
QString arg(Args &&... args) const const
void chop(qsizetype n)
bool isEmpty() const const
QString & remove(QChar ch, Qt::CaseSensitivity cs)
SkipEmptyParts
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void timeout()
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Dec 21 2024 17:04:13 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.