6#include "BookmarkSyncManager.h"
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"
16#include "MarbleDebug.h"
17#include "MarbleDirs.h"
19#include "OwncloudSyncBackend.h"
23#include <QJsonDocument>
25#include <QNetworkAccessManager>
26#include <QNetworkReply>
50 GeoDataPlacemark m_placemarkA;
51 GeoDataPlacemark m_placemarkB;
54class Q_DECL_HIDDEN BookmarkSyncManager::Private
57 Private(BookmarkSyncManager *parent, CloudSyncManager *cloudSyncManager);
59 BookmarkSyncManager *
const m_q;
60 CloudSyncManager *
const m_cloudSyncManager;
80 DiffItem m_conflictItem;
82 BookmarkManager *m_bookmarkManager =
nullptr;
84 bool m_bookmarkSyncEnabled;
96 void uploadBookmarks();
101 void downloadBookmarks();
106 void downloadTimestamp();
112 bool cloudBookmarksModified(
const QString &cloudTimestamp)
const;
124 QString lastSyncedKmlPath()
const;
133 QList<DiffItem> getPlacemarks(GeoDataDocument *document, GeoDataDocument *other, DiffItem::Status diffDirection);
143 QList<DiffItem> getPlacemarks(GeoDataFolder *folder,
QString &path, GeoDataDocument *other, DiffItem::Status diffDirection);
151 const GeoDataPlacemark *findPlacemark(GeoDataContainer *container,
const GeoDataPlacemark &bookmark)
const;
159 void determineDiffStatus(DiffItem &item, GeoDataDocument *document)
const;
186 GeoDataFolder *createFolders(GeoDataContainer *container,
QStringList &pathList);
195 void saveDownloadedToCache(
const QByteArray &kml);
197 void parseTimestamp();
198 void copyLocalToCache();
200 void continueSynchronization();
201 void completeSynchronization();
202 void completeMerge();
203 void completeUpload();
206BookmarkSyncManager::Private::Private(BookmarkSyncManager *parent, CloudSyncManager *cloudSyncManager)
208 , m_cloudSyncManager(cloudSyncManager)
209 , m_bookmarkManager(nullptr)
210 , m_bookmarkSyncEnabled(false)
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");
219BookmarkSyncManager::BookmarkSyncManager(CloudSyncManager *cloudSyncManager)
221 , d(new Private(this, cloudSyncManager))
223 d->m_syncTimer.setInterval(60 * 60 * 1000);
227BookmarkSyncManager::~BookmarkSyncManager()
232QDateTime BookmarkSyncManager::lastSync()
const
234 const QString last = d->lastSyncedKmlPath();
240bool BookmarkSyncManager::isBookmarkSyncEnabled()
const
242 return d->m_bookmarkSyncEnabled;
245void BookmarkSyncManager::setBookmarkSyncEnabled(
bool enabled)
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()) {
257void BookmarkSyncManager::setBookmarkManager(BookmarkManager *manager)
259 d->m_bookmarkManager = manager;
260 connect(manager, SIGNAL(bookmarksChanged()),
this, SLOT(startBookmarkSync()));
264void BookmarkSyncManager::startBookmarkSync()
266 if (!d->m_cloudSyncManager->isSyncEnabled() || !isBookmarkSyncEnabled()) {
270 d->m_syncTimer.start();
271 d->downloadTimestamp();
274QUrl BookmarkSyncManager::Private::endpointUrl(
const QString &endpoint)
const
276 return QUrl(m_cloudSyncManager->apiUrl().toString() +
QLatin1Char(
'/') + endpoint);
279void BookmarkSyncManager::Private::uploadBookmarks()
283 QString word = QStringLiteral(
"----MarbleCloudBoundary");
284 QString boundary = QStringLiteral(
"--%0").
arg(word);
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);
292 QFile bookmarksFile(m_localBookmarksPath);
294 mDebug() <<
"Failed to open file" << bookmarksFile.fileName() <<
". It is either missing or not readable.";
298 QByteArray kmlContent = bookmarksFile.readAll();
299 data.
append(kmlContent + lineBreak + lineBreak);
301 bookmarksFile.close();
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()));
308void BookmarkSyncManager::Private::downloadBookmarks()
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)));
316void BookmarkSyncManager::Private::downloadTimestamp()
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()));
323bool BookmarkSyncManager::Private::cloudBookmarksModified(
const QString &cloudTimestamp)
const
335 return cloudTimestamp != lastSynced;
341void BookmarkSyncManager::Private::clearCache()
343 QDir cacheDir(m_cachePath);
345 if (!fileInfoList.isEmpty()) {
346 for (
const QFileInfo &fileInfo : std::as_const(fileInfoList)) {
347 QFile file(fileInfo.absoluteFilePath());
348 bool removed = file.remove();
350 mDebug() <<
"Could not delete" << file.fileName() <<
"Make sure you have sufficient permissions.";
356QString BookmarkSyncManager::Private::lastSyncedKmlPath()
const
358 QDir cacheDir(m_cachePath);
360 if (!fileInfoList.isEmpty()) {
361 return fileInfoList.last().absoluteFilePath();
367QList<DiffItem> BookmarkSyncManager::Private::getPlacemarks(GeoDataDocument *document, GeoDataDocument *other, DiffItem::Status diffDirection)
370 const auto folders = document->folderList();
371 for (GeoDataFolder *folder : folders) {
373 diffItems.
append(getPlacemarks(folder, path, other, diffDirection));
379QList<DiffItem> BookmarkSyncManager::Private::getPlacemarks(GeoDataFolder *folder,
QString &path, GeoDataDocument *other, DiffItem::Status diffDirection)
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));
388 const auto placemarks = folder->placemarkList();
389 for (GeoDataPlacemark *placemark : placemarks) {
391 diffItem.m_path =
path;
392 diffItem.m_placemarkA = *placemark;
393 switch (diffDirection) {
394 case DiffItem::Source:
395 diffItem.m_origin = DiffItem::Destination;
397 case DiffItem::Destination:
398 diffItem.m_origin = DiffItem::Source;
404 determineDiffStatus(diffItem, other);
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);
415const GeoDataPlacemark *BookmarkSyncManager::Private::findPlacemark(GeoDataContainer *container,
const GeoDataPlacemark &bookmark)
const
417 const auto placemarks = container->placemarkList();
418 for (GeoDataPlacemark *placemark : placemarks) {
419 if (EARTH_RADIUS * placemark->coordinate().sphericalDistanceTo(bookmark.coordinate()) <= 1) {
424 const auto folders = container->folderList();
425 for (GeoDataFolder *folder : folders) {
426 const GeoDataPlacemark *placemark = findPlacemark(folder, bookmark);
435void BookmarkSyncManager::Private::determineDiffStatus(DiffItem &item, GeoDataDocument *document)
const
437 const GeoDataPlacemark *
match = findPlacemark(document, item.m_placemarkA);
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;
450 item.m_action = DiffItem::NoAction;
453 switch (item.m_origin) {
454 case DiffItem::Source:
455 item.m_action = DiffItem::Deleted;
456 item.m_placemarkB = item.m_placemarkA;
458 case DiffItem::Destination:
459 item.m_action = DiffItem::Created;
467 QFile fileB(destinationPath);
469 mDebug() <<
"Could not open file " << fileB.fileName();
471 return diff(sourcePath, &fileB);
476 QFile fileA(sourcePath);
478 mDebug() <<
"Could not open file " << fileA.fileName();
481 return diff(&fileA, fileB);
486 QFile fileB(destinationPath);
488 mDebug() <<
"Could not open file " << fileB.fileName();
491 return diff(source, &fileB);
496 GeoDataParser parserA(GeoData_KML);
498 auto documentA =
dynamic_cast<GeoDataDocument *
>(parserA.releaseDocument());
500 GeoDataParser parserB(GeoData_KML);
502 auto documentB =
dynamic_cast<GeoDataDocument *
>(parserB.releaseDocument());
504 QList<DiffItem> diffItems = getPlacemarks(documentA, documentB, DiffItem::Destination);
505 diffItems.
append(getPlacemarks(documentB, documentA, DiffItem::Source));
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;
522void BookmarkSyncManager::Private::merge()
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;
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) {
534 }
else if (itemB.m_action == DiffItem::Changed) {
541 m_merged.append(other);
542 }
else if (!deleted) {
543 m_merged.append(itemA);
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;
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)) {
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);
571 switch (itemA.m_action) {
572 case DiffItem::Changed:
573 mergeItem->setActionA(MergeItem::Changed);
575 case DiffItem::Deleted:
576 mergeItem->setActionA(MergeItem::Deleted);
582 switch (other.m_action) {
583 case DiffItem::Changed:
584 mergeItem->setActionB(MergeItem::Changed);
586 case DiffItem::Deleted:
587 mergeItem->setActionB(MergeItem::Deleted);
593 Q_EMIT m_q->mergeConflict(mergeItem);
598 if (!m_diffA.isEmpty()) {
599 m_diffA.removeFirst();
603 for (
const DiffItem &itemB : std::as_const(m_diffB)) {
604 if (itemB.m_action == DiffItem::Created) {
605 m_merged.append(itemB);
612GeoDataFolder *BookmarkSyncManager::Private::createFolders(GeoDataContainer *container,
QStringList &pathList)
614 GeoDataFolder *folder =
nullptr;
615 if (pathList.
count() > 0) {
618 const auto otherFolders = container->folderList();
619 for (GeoDataFolder *otherFolder : otherFolders) {
620 if (otherFolder->name() == name) {
621 folder = otherFolder;
625 if (folder ==
nullptr) {
626 folder =
new GeoDataFolder();
627 folder->setName(name);
628 container->append(folder);
636 return createFolders(folder, pathList);
639GeoDataDocument *BookmarkSyncManager::Private::constructDocument(
const QList<DiffItem> &mergedList)
641 auto document =
new GeoDataDocument();
642 document->setName(tr(
"Bookmarks"));
644 for (
const DiffItem &item : mergedList) {
645 auto placemark =
new GeoDataPlacemark(item.m_placemarkA);
647 GeoDataFolder *folder = createFolders(document, splitten);
648 folder->append(placemark);
654void BookmarkSyncManager::resolveConflict(MergeItem *item)
658 switch (item->resolution()) {
660 if (!d->m_diffA.isEmpty()) {
661 diffItem = d->m_diffA.first();
666 diffItem = d->m_conflictItem;
672 if (diffItem.m_action != DiffItem::Deleted) {
673 d->m_merged.append(diffItem);
676 if (!d->m_diffA.isEmpty()) {
677 d->m_diffA.removeFirst();
683void BookmarkSyncManager::Private::saveDownloadedToCache(
const QByteArray &kml)
685 QString localBookmarksDir = m_localBookmarksPath;
686 QDir().
mkdir(localBookmarksDir.
remove(QStringLiteral(
"bookmarks.kml")));
687 QFile bookmarksFile(m_localBookmarksPath);
689 mDebug() <<
"Failed to open file" << bookmarksFile.fileName() <<
". It is either missing or not readable.";
693 bookmarksFile.write(kml);
694 bookmarksFile.close();
698void BookmarkSyncManager::Private::parseTimestamp()
703 m_cloudTimestamp = dataValue.
toString();
704 mDebug() <<
"Remote bookmark timestamp is " << m_cloudTimestamp;
705 continueSynchronization();
707void BookmarkSyncManager::Private::copyLocalToCache()
712 QFile bookmarksFile(m_localBookmarksPath);
713 bookmarksFile.copy(QStringLiteral(
"%0/%1.kml").arg(m_cachePath, m_cloudTimestamp));
717void BookmarkSyncManager::Private::continueSynchronization()
719 bool cloudModified = cloudBookmarksModified(m_cloudTimestamp);
723 QString lastSyncedPath = lastSyncedKmlPath();
724 if (lastSyncedPath.
isEmpty()) {
725 mDebug() <<
"Never synced. Uploading bookmarks.";
729 bool localModified =
false;
730 for (
const DiffItem &item : std::as_const(diffList)) {
731 if (item.m_action != DiffItem::NoAction) {
732 localModified =
true;
737 mDebug() <<
"Local modifications, uploading.";
744void BookmarkSyncManager::Private::completeSynchronization()
746 mDebug() <<
"Merging remote and local bookmark file";
747 QString lastSyncedPath = lastSyncedKmlPath();
748 QFile localBookmarksFile(m_localBookmarksPath);
749 QByteArray result = m_downloadReply->readAll();
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);
759 saveDownloadedToCache(result);
763 m_diffA = diff(lastSyncedPath, m_localBookmarksPath);
764 m_diffB = diff(lastSyncedPath, &buffer);
771void BookmarkSyncManager::Private::completeMerge()
773 QFile localBookmarksFile(m_localBookmarksPath);
774 GeoDataDocument *doc = constructDocument(m_merged);
776 localBookmarksFile.remove();
778 writer.write(&localBookmarksFile, doc);
779 localBookmarksFile.close();
783void BookmarkSyncManager::Private::completeUpload()
788 m_cloudTimestamp = dataValue.
toString();
789 mDebug() <<
"Uploaded bookmarks to remote server. Timestamp is " << m_cloudTimestamp;
791 Q_EMIT m_q->syncComplete();
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
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
QString arg(Args &&... args) const const
bool isEmpty() const const
QString & remove(QChar ch, Qt::CaseSensitivity cs)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)