Marble

BookmarkSyncManager.cpp
1 // SPDX-License-Identifier: LGPL-2.1-or-later
2 //
3 // SPDX-FileCopyrightText: 2013 Utku Aydın <[email protected]>
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 
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 
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 
226 BookmarkSyncManager::~BookmarkSyncManager()
227 {
228  delete d;
229 }
230 
231 QDateTime BookmarkSyncManager::lastSync() const
232 {
233  const QString last = d->lastSyncedKmlPath();
234  if (last.isEmpty())
235  return QDateTime();
236  return QFileInfo(last).metadataChangeTime();
237 }
238 
239 bool BookmarkSyncManager::isBookmarkSyncEnabled() const
240 {
241  return d->m_bookmarkSyncEnabled;
242 }
243 
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 
256 void BookmarkSyncManager::setBookmarkManager(BookmarkManager *manager)
257 {
258  d->m_bookmarkManager = manager;
259  connect( manager, SIGNAL(bookmarksChanged()), this, SLOT(startBookmarkSync()) );
260  startBookmarkSync();
261 }
262 
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 
274 QUrl BookmarkSyncManager::Private::endpointUrl( const QString &endpoint ) const
275 {
276  return QUrl(m_cloudSyncManager->apiUrl().toString() + QLatin1Char('/') + endpoint);
277 }
278 
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 
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 
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 
329 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",
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 
345 void BookmarkSyncManager::Private::clearCache()
346 {
347  QDir cacheDir( m_cachePath );
348  QFileInfoList fileInfoList = cacheDir.entryInfoList(
349  QStringList() << "*.kml",
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 
363 QString BookmarkSyncManager::Private::lastSyncedKmlPath() const
364 {
365  QDir cacheDir( m_cachePath );
366  QFileInfoList fileInfoList = cacheDir.entryInfoList(
367  QStringList() << "*.kml",
369  if( !fileInfoList.isEmpty() ) {
370  return fileInfoList.last().absoluteFilePath();
371  } else {
372  return QString();
373  }
374 }
375 
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 
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 
421 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 
439 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 
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 
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 
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 
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 
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 
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 
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 
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  Q_FALLTHROUGH();
670  case MergeItem::B:
671  diffItem = d->m_conflictItem;
672  break;
673  default:
674  return; // It shouldn't happen.
675  }
676 
677  if( diffItem.m_action != DiffItem::Deleted ) {
678  d->m_merged.append( diffItem );
679  }
680 
681  if( !d->m_diffA.isEmpty() ) {
682  d->m_diffA.removeFirst();
683  }
684 
685  d->merge();
686 }
687 
688 void BookmarkSyncManager::Private::saveDownloadedToCache( const QByteArray &kml )
689 {
690  QString localBookmarksDir = m_localBookmarksPath;
691  QDir().mkdir( localBookmarksDir.remove( "bookmarks.kml" ) );
692  QFile bookmarksFile( m_localBookmarksPath );
693  if( !bookmarksFile.open( QFile::ReadWrite ) ) {
694  mDebug() << "Failed to open file" << bookmarksFile.fileName()
695  << ". It is either missing or not readable.";
696  return;
697  }
698 
699  bookmarksFile.write( kml );
700  bookmarksFile.close();
701  copyLocalToCache();
702 }
703 
704 void BookmarkSyncManager::Private::parseTimestamp()
705 {
706  QJsonDocument jsonDoc = QJsonDocument::fromJson(m_timestampReply->readAll());
707  QJsonValue dataValue = jsonDoc.object().value(QStringLiteral("data"));
708 
709  m_cloudTimestamp = dataValue.toString();
710  mDebug() << "Remote bookmark timestamp is " << m_cloudTimestamp;
711  continueSynchronization();
712 }
713 void BookmarkSyncManager::Private::copyLocalToCache()
714 {
715  QDir().mkpath( m_cachePath );
716  clearCache();
717 
718  QFile bookmarksFile( m_localBookmarksPath );
719  bookmarksFile.copy( QString( "%0/%1.kml" ).arg( m_cachePath, m_cloudTimestamp ) );
720 }
721 
722 // Bookmark synchronization steps
723 void BookmarkSyncManager::Private::continueSynchronization()
724 {
725  bool cloudModified = cloudBookmarksModified( m_cloudTimestamp );
726  if( cloudModified ) {
727  downloadBookmarks();
728  } else {
729  QString lastSyncedPath = lastSyncedKmlPath();
730  if( lastSyncedPath.isEmpty() ) {
731  mDebug() << "Never synced. Uploading bookmarks.";
732  uploadBookmarks();
733  } else {
734  QList<DiffItem> diffList = diff( lastSyncedPath, m_localBookmarksPath );
735  bool localModified = false;
736  for( const DiffItem &item: diffList ) {
737  if( item.m_action != DiffItem::NoAction ) {
738  localModified = true;
739  }
740  }
741 
742  if( localModified ) {
743  mDebug() << "Local modifications, uploading.";
744  uploadBookmarks();
745  }
746  }
747  }
748 }
749 
750 void BookmarkSyncManager::Private::completeSynchronization()
751 {
752  mDebug() << "Merging remote and local bookmark file";
753  QString lastSyncedPath = lastSyncedKmlPath();
754  QFile localBookmarksFile( m_localBookmarksPath );
755  QByteArray result = m_downloadReply->readAll();
756  QBuffer buffer( &result );
757  buffer.open( QIODevice::ReadOnly );
758 
759  if( lastSyncedPath.isEmpty() ) {
760  if( localBookmarksFile.exists() ) {
761  mDebug() << "Conflict between remote bookmarks and local ones";
762  m_diffA = diff( &buffer, m_localBookmarksPath );
763  m_diffB = diff( m_localBookmarksPath, &buffer );
764  } else {
765  saveDownloadedToCache( result );
766  return;
767  }
768  }
769  else
770  {
771  m_diffA = diff( lastSyncedPath, m_localBookmarksPath );
772  m_diffB = diff( lastSyncedPath, &buffer );
773  }
774 
775  m_merged.clear();
776  merge();
777 }
778 
779 void BookmarkSyncManager::Private::completeMerge()
780 {
781  QFile localBookmarksFile( m_localBookmarksPath );
782  GeoDataDocument *doc = constructDocument( m_merged );
783  GeoWriter writer;
784  localBookmarksFile.remove();
785  localBookmarksFile.open( QFile::ReadWrite );
786  writer.write( &localBookmarksFile, doc );
787  localBookmarksFile.close();
788  uploadBookmarks();
789 }
790 
791 void BookmarkSyncManager::Private::completeUpload()
792 {
793  QJsonDocument jsonDoc = QJsonDocument::fromJson(m_uploadReply->readAll());
794  QJsonValue dataValue = jsonDoc.object().value(QStringLiteral("data"));
795 
796  m_cloudTimestamp = dataValue.toString();
797  mDebug() << "Uploaded bookmarks to remote server. Timestamp is " << m_cloudTimestamp;
798  copyLocalToCache();
799  emit m_q->syncComplete();
800 }
801 
802 }
803 
804 #include "moc_BookmarkSyncManager.cpp"
void append(const T &value)
QJsonObject object() const const
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
QByteArray & append(char ch)
int count(const T &value) const const
void chop(int n)
virtual bool open(QIODevice::OpenMode mode)
bool mkdir(const QString &dirName) const const
QString toString() const const
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QDateTime metadataChangeTime() const const
T takeFirst()
bool isEmpty() const const
QStringView merge(QStringView lhs, QStringView rhs)
bool mkpath(const QString &dirPath) const const
bool isEmpty() const const
QJsonValue value(const QString &key) const const
Binds a QML item to a specific geodetic location in screen coordinates.
const AKONADI_MIME_EXPORT char Deleted[]
T & last()
QString & remove(int position, int n)
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
QString path(const QString &relativePath)
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
QString name(StandardShortcut id)
QStringList entryList(QDir::Filters filters, QDir::SortFlags sort) const const
QDebug mDebug()
a function to replace qDebug() in Marble library code
Definition: MarbleDebug.cpp:31
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Mon Oct 2 2023 03:52:07 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.