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