File indexing completed on 2024-05-19 07:37:36
0001 // SPDX-License-Identifier: LGPL-2.1-or-later 0002 // 0003 // SPDX-FileCopyrightText: 2013 Utku Aydın <utkuaydin34@gmail.com> 0004 // 0005 0006 #include "BookmarkSyncManager.h" 0007 0008 #include "GeoWriter.h" 0009 #include "MarbleDirs.h" 0010 #include "MarbleDebug.h" 0011 #include "GeoDataParser.h" 0012 #include "GeoDataFolder.h" 0013 #include "GeoDataDocument.h" 0014 #include "GeoDataLookAt.h" 0015 #include "CloudSyncManager.h" 0016 #include "GeoDataCoordinates.h" 0017 #include "OwncloudSyncBackend.h" 0018 #include "MarbleModel.h" 0019 #include "BookmarkManager.h" 0020 0021 #include <QFile> 0022 #include <QBuffer> 0023 #include <QJsonDocument> 0024 #include <QJsonObject> 0025 #include <QNetworkAccessManager> 0026 #include <QNetworkReply> 0027 #include <QTimer> 0028 0029 namespace Marble { 0030 0031 class DiffItem 0032 { 0033 public: 0034 enum Action { 0035 NoAction, 0036 Created, 0037 Changed, 0038 Deleted 0039 }; 0040 0041 enum Status { 0042 Source, 0043 Destination 0044 }; 0045 0046 QString m_path; 0047 Action m_action; 0048 Status m_origin; 0049 GeoDataPlacemark m_placemarkA; 0050 GeoDataPlacemark m_placemarkB; 0051 }; 0052 0053 class Q_DECL_HIDDEN BookmarkSyncManager::Private 0054 { 0055 public: 0056 Private( BookmarkSyncManager* parent, CloudSyncManager *cloudSyncManager ); 0057 0058 BookmarkSyncManager* m_q; 0059 CloudSyncManager *m_cloudSyncManager; 0060 0061 QNetworkAccessManager m_network; 0062 QString m_uploadEndpoint; 0063 QString m_downloadEndpoint; 0064 QString m_timestampEndpoint; 0065 0066 QNetworkReply* m_uploadReply; 0067 QNetworkReply* m_downloadReply; 0068 QNetworkReply* m_timestampReply; 0069 0070 QString m_cloudTimestamp; 0071 0072 QString m_cachePath; 0073 QString m_localBookmarksPath; 0074 QString m_bookmarksTimestamp; 0075 0076 QList<DiffItem> m_diffA; 0077 QList<DiffItem> m_diffB; 0078 QList<DiffItem> m_merged; 0079 DiffItem m_conflictItem; 0080 0081 BookmarkManager* m_bookmarkManager; 0082 QTimer m_syncTimer; 0083 bool m_bookmarkSyncEnabled; 0084 0085 /** 0086 * Returns an API endpoint 0087 * @param endpoint Endpoint itself without server info 0088 * @return Complete API URL as QUrl 0089 */ 0090 QUrl endpointUrl( const QString &endpoint ) const; 0091 0092 /** 0093 * Uploads local bookmarks.kml to cloud. 0094 */ 0095 void uploadBookmarks(); 0096 0097 /** 0098 * Downloads bookmarks.kml from cloud. 0099 */ 0100 void downloadBookmarks(); 0101 0102 /** 0103 * Gets cloud bookmarks.kml's timestamp from cloud. 0104 */ 0105 void downloadTimestamp(); 0106 0107 /** 0108 * Compares cloud bookmarks.kml's timestamp to last synced bookmarks.kml's timestamp. 0109 * @return true if cloud one is different from last synced one. 0110 */ 0111 bool cloudBookmarksModified( const QString &cloudTimestamp ) const; 0112 0113 /** 0114 * Removes all KMLs in the cache except the 0115 * one with youngest timestamp. 0116 */ 0117 void clearCache(); 0118 0119 /** 0120 * Finds the last synced bookmarks.kml file and returns its path 0121 * @return Path of last synced bookmarks.kml file. 0122 */ 0123 QString lastSyncedKmlPath() const; 0124 0125 /** 0126 * Gets all placemarks in a document as DiffItems, compares them to another document and puts the result in a list. 0127 * @param document The document whose placemarks will be compared to another document's placemarks. 0128 * @param other The document whose placemarks will be compared to the first document's placemarks. 0129 * @param diffDirection Direction of comparison, e.g. must be DiffItem::Destination if direction is source to destination. 0130 * @return A list of DiffItems 0131 */ 0132 QList<DiffItem> getPlacemarks(GeoDataDocument *document, GeoDataDocument *other, DiffItem::Status diffDirection ); 0133 0134 /** 0135 * Gets all placemarks in a document as DiffItems, compares them to another document and puts the result in a list. 0136 * @param folder The folder whose placemarks will be compared to another document's placemarks. 0137 * @param path Path of the folder. 0138 * @param other The document whose placemarks will be compared to the first document's placemarks. 0139 * @param diffDirection Direction of comparison, e.g. must be DiffItem::Destination if direction is source to destination. 0140 * @return A list of DiffItems 0141 */ 0142 QList<DiffItem> getPlacemarks( GeoDataFolder *folder, QString &path, GeoDataDocument *other, DiffItem::Status diffDirection ); 0143 0144 /** 0145 * Finds the placemark which has the same coordinates with given bookmark 0146 * @param container Container of placemarks which will be compared. Can be document or folder. 0147 * @param bookmark The bookmark whose counterpart will be searched in the container. 0148 * @return Counterpart of the given placemark. 0149 */ 0150 const GeoDataPlacemark* findPlacemark( GeoDataContainer* container, const GeoDataPlacemark &bookmark ) const; 0151 0152 /** 0153 * Determines the status (created, deleted, changed or unchanged) of given DiffItem 0154 * by comparing the item's placemark with placemarks of given GeoDataDocument. 0155 * @param item The item whose status will be determined. 0156 * @param document The document whose placemarks will be used to determine DiffItem's status. 0157 */ 0158 void determineDiffStatus( DiffItem &item, GeoDataDocument* document ) const; 0159 0160 /** 0161 * Finds differences between two bookmark files. 0162 * @param sourcePath Source bookmark 0163 * @param destinationPath Destination bookmark 0164 * @return A list of differences 0165 */ 0166 QList<DiffItem> diff( QString &sourcePath, QString &destinationPath ); 0167 QList<DiffItem> diff( QString &sourcePath, QIODevice* destination ); 0168 QList<DiffItem> diff( QIODevice* source, QString &destinationPath ); 0169 QList<DiffItem> diff( QIODevice *source, QIODevice* destination ); 0170 0171 /** 0172 * Merges two diff lists. 0173 * @param diffListA First diff list. 0174 * @param diffListB Second diff list. 0175 * @return Merged DiffItems. 0176 */ 0177 void merge(); 0178 0179 /** 0180 * Creates GeoDataFolders using strings in path list. 0181 * @param container Container which created GeoDataFolder will be attached to. 0182 * @param pathList Names of folders. Note that each item will be the child of the previous one. 0183 * @return A pointer to created folder. 0184 */ 0185 GeoDataFolder* createFolders( GeoDataContainer *container, QStringList &pathList ); 0186 0187 /** 0188 * Creates a GeoDataDocument using a list of DiffItems. 0189 * @param mergedList DiffItems which will be used as placemarks. 0190 * @return A pointer to created document. 0191 */ 0192 GeoDataDocument* constructDocument( const QList<DiffItem> &mergedList ); 0193 0194 void saveDownloadedToCache( const QByteArray &kml ); 0195 0196 void parseTimestamp(); 0197 void copyLocalToCache(); 0198 0199 void continueSynchronization(); 0200 void completeSynchronization(); 0201 void completeMerge(); 0202 void completeUpload(); 0203 }; 0204 0205 BookmarkSyncManager::Private::Private(BookmarkSyncManager *parent, CloudSyncManager *cloudSyncManager ) : 0206 m_q( parent ), 0207 m_cloudSyncManager( cloudSyncManager ), 0208 m_bookmarkManager( nullptr ), 0209 m_bookmarkSyncEnabled( false ) 0210 { 0211 m_cachePath = MarbleDirs::localPath() + QLatin1String("/cloudsync/cache/bookmarks"); 0212 m_localBookmarksPath = MarbleDirs::localPath() + QLatin1String("/bookmarks/bookmarks.kml"); 0213 m_downloadEndpoint = "bookmarks/kml"; 0214 m_uploadEndpoint = "bookmarks/update"; 0215 m_timestampEndpoint = "bookmarks/timestamp"; 0216 } 0217 0218 BookmarkSyncManager::BookmarkSyncManager( CloudSyncManager *cloudSyncManager ) : 0219 QObject(), 0220 d( new Private( this, cloudSyncManager ) ) 0221 { 0222 d->m_syncTimer.setInterval( 60 * 60 * 1000 ); // 1 hour. TODO: Make this configurable. 0223 connect( &d->m_syncTimer, SIGNAL(timeout()), this, SLOT(startBookmarkSync()) ); 0224 } 0225 0226 BookmarkSyncManager::~BookmarkSyncManager() 0227 { 0228 delete d; 0229 } 0230 0231 QDateTime BookmarkSyncManager::lastSync() const 0232 { 0233 const QString last = d->lastSyncedKmlPath(); 0234 if (last.isEmpty()) 0235 return QDateTime(); 0236 return QFileInfo(last).metadataChangeTime(); 0237 } 0238 0239 bool BookmarkSyncManager::isBookmarkSyncEnabled() const 0240 { 0241 return d->m_bookmarkSyncEnabled; 0242 } 0243 0244 void BookmarkSyncManager::setBookmarkSyncEnabled( bool enabled ) 0245 { 0246 bool const old_state = isBookmarkSyncEnabled(); 0247 d->m_bookmarkSyncEnabled = enabled; 0248 if ( old_state != isBookmarkSyncEnabled() ) { 0249 emit bookmarkSyncEnabledChanged( d->m_bookmarkSyncEnabled ); 0250 if ( isBookmarkSyncEnabled() ) { 0251 startBookmarkSync(); 0252 } 0253 } 0254 } 0255 0256 void BookmarkSyncManager::setBookmarkManager(BookmarkManager *manager) 0257 { 0258 d->m_bookmarkManager = manager; 0259 connect( manager, SIGNAL(bookmarksChanged()), this, SLOT(startBookmarkSync()) ); 0260 startBookmarkSync(); 0261 } 0262 0263 void BookmarkSyncManager::startBookmarkSync() 0264 { 0265 if ( !d->m_cloudSyncManager->isSyncEnabled() || !isBookmarkSyncEnabled() ) 0266 { 0267 return; 0268 } 0269 0270 d->m_syncTimer.start(); 0271 d->downloadTimestamp(); 0272 } 0273 0274 QUrl BookmarkSyncManager::Private::endpointUrl( const QString &endpoint ) const 0275 { 0276 return QUrl(m_cloudSyncManager->apiUrl().toString() + QLatin1Char('/') + endpoint); 0277 } 0278 0279 void BookmarkSyncManager::Private::uploadBookmarks() 0280 { 0281 QByteArray data; 0282 QByteArray lineBreak = "\r\n"; 0283 QString word = "----MarbleCloudBoundary"; 0284 QString boundary = QString( "--%0" ).arg( word ); 0285 QNetworkRequest request( endpointUrl( m_uploadEndpoint ) ); 0286 request.setHeader( QNetworkRequest::ContentTypeHeader, QString( "multipart/form-data; boundary=%0" ).arg( word ) ); 0287 0288 data.append( QString( boundary + lineBreak ).toUtf8() ); 0289 data.append( "Content-Disposition: form-data; name=\"bookmarks\"; filename=\"bookmarks.kml\"" + lineBreak ); 0290 data.append( "Content-Type: application/vnd.google-earth.kml+xml" + lineBreak + lineBreak ); 0291 0292 QFile bookmarksFile( m_localBookmarksPath ); 0293 if( !bookmarksFile.open( QFile::ReadOnly ) ) { 0294 mDebug() << "Failed to open file" << bookmarksFile.fileName() 0295 << ". It is either missing or not readable."; 0296 return; 0297 } 0298 0299 QByteArray kmlContent = bookmarksFile.readAll(); 0300 data.append( kmlContent + lineBreak + lineBreak ); 0301 data.append( QString( boundary ).toUtf8() ); 0302 bookmarksFile.close(); 0303 0304 m_uploadReply = m_network.post( request, data ); 0305 connect( m_uploadReply, SIGNAL(uploadProgress(qint64,qint64)), 0306 m_q, SIGNAL(uploadProgress(qint64,qint64)) ); 0307 connect( m_uploadReply, SIGNAL(finished()), 0308 m_q, SLOT(completeUpload()) ); 0309 } 0310 0311 void BookmarkSyncManager::Private::downloadBookmarks() 0312 { 0313 QNetworkRequest request( endpointUrl( m_downloadEndpoint ) ); 0314 m_downloadReply = m_network.get( request ); 0315 connect( m_downloadReply, SIGNAL(finished()), 0316 m_q, SLOT(completeSynchronization()) ); 0317 connect( m_downloadReply, SIGNAL(downloadProgress(qint64,qint64)), 0318 m_q, SIGNAL(downloadProgress(qint64,qint64)) ); 0319 } 0320 0321 void BookmarkSyncManager::Private::downloadTimestamp() 0322 { 0323 mDebug() << "Determining remote bookmark state."; 0324 m_timestampReply = m_network.get( QNetworkRequest( endpointUrl( m_timestampEndpoint ) ) ); 0325 connect( m_timestampReply, SIGNAL(finished()), 0326 m_q, SLOT(parseTimestamp()) ); 0327 } 0328 0329 bool BookmarkSyncManager::Private::cloudBookmarksModified( const QString &cloudTimestamp ) const 0330 { 0331 QStringList entryList = QDir( m_cachePath ).entryList( 0332 // TODO: replace with regex filter that only 0333 // allows timestamp filenames 0334 QStringList() << "*.kml", 0335 QDir::NoFilter, QDir::Name ); 0336 if( !entryList.isEmpty() ) { 0337 QString lastSynced = entryList.last(); 0338 lastSynced.chop( 4 ); 0339 return cloudTimestamp != lastSynced; 0340 } else { 0341 return true; // That will let cloud one get downloaded. 0342 } 0343 } 0344 0345 void BookmarkSyncManager::Private::clearCache() 0346 { 0347 QDir cacheDir( m_cachePath ); 0348 QFileInfoList fileInfoList = cacheDir.entryInfoList( 0349 QStringList() << "*.kml", 0350 QDir::NoFilter, QDir::Name ); 0351 if( !fileInfoList.isEmpty() ) { 0352 for ( const QFileInfo& fileInfo: fileInfoList ) { 0353 QFile file( fileInfo.absoluteFilePath() ); 0354 bool removed = file.remove(); 0355 if( !removed ) { 0356 mDebug() << "Could not delete" << file.fileName() << 0357 "Make sure you have sufficient permissions."; 0358 } 0359 } 0360 } 0361 } 0362 0363 QString BookmarkSyncManager::Private::lastSyncedKmlPath() const 0364 { 0365 QDir cacheDir( m_cachePath ); 0366 QFileInfoList fileInfoList = cacheDir.entryInfoList( 0367 QStringList() << "*.kml", 0368 QDir::NoFilter, QDir::Name ); 0369 if( !fileInfoList.isEmpty() ) { 0370 return fileInfoList.last().absoluteFilePath(); 0371 } else { 0372 return QString(); 0373 } 0374 } 0375 0376 QList<DiffItem> BookmarkSyncManager::Private::getPlacemarks( GeoDataDocument *document, GeoDataDocument *other, DiffItem::Status diffDirection ) 0377 { 0378 QList<DiffItem> diffItems; 0379 for ( GeoDataFolder *folder: document->folderList() ) { 0380 QString path = QString( "/%0" ).arg( folder->name() ); 0381 diffItems.append( getPlacemarks( folder, path, other, diffDirection ) ); 0382 } 0383 0384 return diffItems; 0385 } 0386 0387 QList<DiffItem> BookmarkSyncManager::Private::getPlacemarks( GeoDataFolder *folder, QString &path, GeoDataDocument *other, DiffItem::Status diffDirection ) 0388 { 0389 QList<DiffItem> diffItems; 0390 for ( GeoDataFolder *subFolder: folder->folderList() ) { 0391 QString newPath = QString( "%0/%1" ).arg( path, subFolder->name() ); 0392 diffItems.append( getPlacemarks( subFolder, newPath, other, diffDirection ) ); 0393 } 0394 0395 for( GeoDataPlacemark *placemark: folder->placemarkList() ) { 0396 DiffItem diffItem; 0397 diffItem.m_path = path; 0398 diffItem.m_placemarkA = *placemark; 0399 switch ( diffDirection ) { 0400 case DiffItem::Source: 0401 diffItem.m_origin = DiffItem::Destination; 0402 break; 0403 case DiffItem::Destination: 0404 diffItem.m_origin = DiffItem::Source; 0405 break; 0406 default: 0407 break; 0408 } 0409 0410 determineDiffStatus( diffItem, other ); 0411 0412 if( !( diffItem.m_action == DiffItem::NoAction && diffItem.m_origin == DiffItem::Destination ) 0413 && !( diffItem.m_action == DiffItem::Changed && diffItem.m_origin == DiffItem::Source ) ) { 0414 diffItems.append( diffItem ); 0415 } 0416 } 0417 0418 return diffItems; 0419 } 0420 0421 const GeoDataPlacemark* BookmarkSyncManager::Private::findPlacemark( GeoDataContainer* container, const GeoDataPlacemark &bookmark ) const 0422 { 0423 for( GeoDataPlacemark* placemark: container->placemarkList() ) { 0424 if (EARTH_RADIUS * placemark->coordinate().sphericalDistanceTo(bookmark.coordinate()) <= 1) { 0425 return placemark; 0426 } 0427 } 0428 0429 for( GeoDataFolder* folder: container->folderList() ) { 0430 const GeoDataPlacemark* placemark = findPlacemark( folder, bookmark ); 0431 if ( placemark ) { 0432 return placemark; 0433 } 0434 } 0435 0436 return nullptr; 0437 } 0438 0439 void BookmarkSyncManager::Private::determineDiffStatus( DiffItem &item, GeoDataDocument *document ) const 0440 { 0441 const GeoDataPlacemark *match = findPlacemark( document, item.m_placemarkA ); 0442 0443 if( match != nullptr ) { 0444 item.m_placemarkB = *match; 0445 bool nameChanged = item.m_placemarkA.name() != item.m_placemarkB.name(); 0446 bool descChanged = item.m_placemarkA.description() != item.m_placemarkB.description(); 0447 bool lookAtChanged = item.m_placemarkA.lookAt()->latitude() != item.m_placemarkB.lookAt()->latitude() || 0448 item.m_placemarkA.lookAt()->longitude() != item.m_placemarkB.lookAt()->longitude() || 0449 item.m_placemarkA.lookAt()->altitude() != item.m_placemarkB.lookAt()->altitude() || 0450 item.m_placemarkA.lookAt()->range() != item.m_placemarkB.lookAt()->range(); 0451 if( nameChanged || descChanged || lookAtChanged ) { 0452 item.m_action = DiffItem::Changed; 0453 } else { 0454 item.m_action = DiffItem::NoAction; 0455 } 0456 } else { 0457 switch( item.m_origin ) { 0458 case DiffItem::Source: 0459 item.m_action = DiffItem::Deleted; 0460 item.m_placemarkB = item.m_placemarkA; // for conflict purposes 0461 break; 0462 case DiffItem::Destination: 0463 item.m_action = DiffItem::Created; 0464 break; 0465 } 0466 0467 } 0468 } 0469 0470 QList<DiffItem> BookmarkSyncManager::Private::diff( QString &sourcePath, QString &destinationPath ) 0471 { 0472 QFile fileB( destinationPath ); 0473 if( !fileB.open( QFile::ReadOnly ) ) { 0474 mDebug() << "Could not open file " << fileB.fileName(); 0475 } 0476 return diff( sourcePath, &fileB ); 0477 } 0478 0479 QList<DiffItem> BookmarkSyncManager::Private::diff( QString &sourcePath, QIODevice *fileB ) 0480 { 0481 QFile fileA( sourcePath ); 0482 if( !fileA.open( QFile::ReadOnly ) ) { 0483 mDebug() << "Could not open file " << fileA.fileName(); 0484 } 0485 0486 return diff( &fileA, fileB ); 0487 } 0488 0489 QList<DiffItem> BookmarkSyncManager::Private::diff( QIODevice *source, QString &destinationPath ) 0490 { 0491 QFile fileB( destinationPath ); 0492 if( !fileB.open( QFile::ReadOnly ) ) { 0493 mDebug() << "Could not open file " << fileB.fileName(); 0494 } 0495 0496 return diff( source, &fileB ); 0497 } 0498 0499 QList<DiffItem> BookmarkSyncManager::Private::diff( QIODevice *fileA, QIODevice *fileB ) 0500 { 0501 GeoDataParser parserA( GeoData_KML ); 0502 parserA.read( fileA ); 0503 GeoDataDocument *documentA = dynamic_cast<GeoDataDocument*>( parserA.releaseDocument() ); 0504 0505 GeoDataParser parserB( GeoData_KML ); 0506 parserB.read( fileB ); 0507 GeoDataDocument *documentB = dynamic_cast<GeoDataDocument*>( parserB.releaseDocument() ); 0508 0509 QList<DiffItem> diffItems = getPlacemarks( documentA, documentB, DiffItem::Destination ); // Compare old to new 0510 diffItems.append( getPlacemarks( documentB, documentA, DiffItem::Source ) ); // Compare new to old 0511 0512 // Compare paths 0513 for( int i = 0; i < diffItems.count(); i++ ) { 0514 for( int p = i + 1; p < diffItems.count(); p++ ) { 0515 if( ( diffItems[i].m_origin == DiffItem::Source ) 0516 && ( diffItems[i].m_action == DiffItem::NoAction ) 0517 && ( EARTH_RADIUS * diffItems[i].m_placemarkA.coordinate().sphericalDistanceTo(diffItems[p].m_placemarkB.coordinate()) <= 1 ) 0518 && ( EARTH_RADIUS * diffItems[i].m_placemarkB.coordinate().sphericalDistanceTo(diffItems[p].m_placemarkA.coordinate()) <= 1 ) 0519 && ( diffItems[i].m_path != diffItems[p].m_path ) ) { 0520 diffItems[p].m_action = DiffItem::Changed; 0521 } 0522 } 0523 } 0524 0525 return diffItems; 0526 } 0527 0528 void BookmarkSyncManager::Private::merge() 0529 { 0530 for( const DiffItem &itemA: m_diffA ) { 0531 if( itemA.m_action == DiffItem::NoAction ) { 0532 bool deleted = false; 0533 bool changed = false; 0534 DiffItem other; 0535 0536 for( const DiffItem &itemB: m_diffB ) { 0537 if( EARTH_RADIUS * itemA.m_placemarkA.coordinate().sphericalDistanceTo(itemB.m_placemarkA.coordinate()) <= 1 ) { 0538 if( itemB.m_action == DiffItem::Deleted ) { 0539 deleted = true; 0540 } else if( itemB.m_action == DiffItem::Changed ) { 0541 changed = true; 0542 other = itemB; 0543 } 0544 } 0545 } 0546 if( changed ) { 0547 m_merged.append( other ); 0548 } else if( !deleted ) { 0549 m_merged.append( itemA ); 0550 } 0551 } else if( itemA.m_action == DiffItem::Created ) { 0552 m_merged.append( itemA ); 0553 } else if( itemA.m_action == DiffItem::Changed || itemA.m_action == DiffItem::Deleted ) { 0554 bool conflict = false; 0555 DiffItem other; 0556 0557 for( const DiffItem &itemB: m_diffB ) { 0558 if (EARTH_RADIUS * itemA.m_placemarkB.coordinate().sphericalDistanceTo(itemB.m_placemarkB.coordinate()) <= 1) { 0559 if( ( itemA.m_action == DiffItem::Changed && ( itemB.m_action == DiffItem::Changed || itemB.m_action == DiffItem::Deleted ) ) 0560 || ( itemA.m_action == DiffItem::Deleted && itemB.m_action == DiffItem::Changed ) ) { 0561 conflict = true; 0562 other = itemB; 0563 } 0564 } 0565 } 0566 0567 if( !conflict && itemA.m_action == DiffItem::Changed ) { 0568 m_merged.append( itemA ); 0569 } else if ( conflict ) { 0570 m_conflictItem = other; 0571 MergeItem *mergeItem = new MergeItem(); 0572 mergeItem->setPathA( itemA.m_path ); 0573 mergeItem->setPathB( other.m_path ); 0574 mergeItem->setPlacemarkA( itemA.m_placemarkA ); 0575 mergeItem->setPlacemarkB( other.m_placemarkA ); 0576 0577 switch( itemA.m_action ) { 0578 case DiffItem::Changed: 0579 mergeItem->setActionA( MergeItem::Changed ); 0580 break; 0581 case DiffItem::Deleted: 0582 mergeItem->setActionA( MergeItem::Deleted ); 0583 break; 0584 default: 0585 break; 0586 } 0587 0588 switch( other.m_action ) { 0589 case DiffItem::Changed: 0590 mergeItem->setActionB( MergeItem::Changed ); 0591 break; 0592 case DiffItem::Deleted: 0593 mergeItem->setActionB( MergeItem::Deleted ); 0594 break; 0595 default: 0596 break; 0597 } 0598 0599 emit m_q->mergeConflict( mergeItem ); 0600 return; 0601 } 0602 } 0603 0604 if( !m_diffA.isEmpty() ) { 0605 m_diffA.removeFirst(); 0606 } 0607 } 0608 0609 for( const DiffItem &itemB: m_diffB ) { 0610 if( itemB.m_action == DiffItem::Created ) { 0611 m_merged.append( itemB ); 0612 } 0613 } 0614 0615 completeMerge(); 0616 } 0617 0618 GeoDataFolder* BookmarkSyncManager::Private::createFolders( GeoDataContainer *container, QStringList &pathList ) 0619 { 0620 GeoDataFolder *folder = nullptr; 0621 if( pathList.count() > 0 ) { 0622 QString name = pathList.takeFirst(); 0623 0624 for( GeoDataFolder *otherFolder: container->folderList() ) { 0625 if( otherFolder->name() == name ) { 0626 folder = otherFolder; 0627 } 0628 } 0629 0630 if( folder == nullptr ) { 0631 folder = new GeoDataFolder(); 0632 folder->setName( name ); 0633 container->append( folder ); 0634 } 0635 0636 if( pathList.count() == 0 ) { 0637 return folder; 0638 } 0639 } 0640 0641 return createFolders( folder, pathList ); 0642 } 0643 0644 GeoDataDocument* BookmarkSyncManager::Private::constructDocument( const QList<DiffItem> &mergedList ) 0645 { 0646 GeoDataDocument *document = new GeoDataDocument(); 0647 document->setName( tr( "Bookmarks" ) ); 0648 0649 for( const DiffItem &item: mergedList ) { 0650 GeoDataPlacemark *placemark = new GeoDataPlacemark( item.m_placemarkA ); 0651 QStringList splitten = item.m_path.split(QLatin1Char('/'), QString::SkipEmptyParts); 0652 GeoDataFolder *folder = createFolders( document, splitten ); 0653 folder->append( placemark ); 0654 } 0655 0656 return document; 0657 } 0658 0659 void BookmarkSyncManager::resolveConflict( MergeItem *item ) 0660 { 0661 DiffItem diffItem; 0662 0663 switch( item->resolution() ) { 0664 case MergeItem::A: 0665 if( !d->m_diffA.isEmpty() ) { 0666 diffItem = d->m_diffA.first(); 0667 break; 0668 } 0669 Q_FALLTHROUGH(); 0670 case MergeItem::B: 0671 diffItem = d->m_conflictItem; 0672 break; 0673 default: 0674 return; // It shouldn't happen. 0675 } 0676 0677 if( diffItem.m_action != DiffItem::Deleted ) { 0678 d->m_merged.append( diffItem ); 0679 } 0680 0681 if( !d->m_diffA.isEmpty() ) { 0682 d->m_diffA.removeFirst(); 0683 } 0684 0685 d->merge(); 0686 } 0687 0688 void BookmarkSyncManager::Private::saveDownloadedToCache( const QByteArray &kml ) 0689 { 0690 QString localBookmarksDir = m_localBookmarksPath; 0691 QDir().mkdir( localBookmarksDir.remove( "bookmarks.kml" ) ); 0692 QFile bookmarksFile( m_localBookmarksPath ); 0693 if( !bookmarksFile.open( QFile::ReadWrite ) ) { 0694 mDebug() << "Failed to open file" << bookmarksFile.fileName() 0695 << ". It is either missing or not readable."; 0696 return; 0697 } 0698 0699 bookmarksFile.write( kml ); 0700 bookmarksFile.close(); 0701 copyLocalToCache(); 0702 } 0703 0704 void BookmarkSyncManager::Private::parseTimestamp() 0705 { 0706 QJsonDocument jsonDoc = QJsonDocument::fromJson(m_timestampReply->readAll()); 0707 QJsonValue dataValue = jsonDoc.object().value(QStringLiteral("data")); 0708 0709 m_cloudTimestamp = dataValue.toString(); 0710 mDebug() << "Remote bookmark timestamp is " << m_cloudTimestamp; 0711 continueSynchronization(); 0712 } 0713 void BookmarkSyncManager::Private::copyLocalToCache() 0714 { 0715 QDir().mkpath( m_cachePath ); 0716 clearCache(); 0717 0718 QFile bookmarksFile( m_localBookmarksPath ); 0719 bookmarksFile.copy( QString( "%0/%1.kml" ).arg( m_cachePath, m_cloudTimestamp ) ); 0720 } 0721 0722 // Bookmark synchronization steps 0723 void BookmarkSyncManager::Private::continueSynchronization() 0724 { 0725 bool cloudModified = cloudBookmarksModified( m_cloudTimestamp ); 0726 if( cloudModified ) { 0727 downloadBookmarks(); 0728 } else { 0729 QString lastSyncedPath = lastSyncedKmlPath(); 0730 if( lastSyncedPath.isEmpty() ) { 0731 mDebug() << "Never synced. Uploading bookmarks."; 0732 uploadBookmarks(); 0733 } else { 0734 QList<DiffItem> diffList = diff( lastSyncedPath, m_localBookmarksPath ); 0735 bool localModified = false; 0736 for( const DiffItem &item: diffList ) { 0737 if( item.m_action != DiffItem::NoAction ) { 0738 localModified = true; 0739 } 0740 } 0741 0742 if( localModified ) { 0743 mDebug() << "Local modifications, uploading."; 0744 uploadBookmarks(); 0745 } 0746 } 0747 } 0748 } 0749 0750 void BookmarkSyncManager::Private::completeSynchronization() 0751 { 0752 mDebug() << "Merging remote and local bookmark file"; 0753 QString lastSyncedPath = lastSyncedKmlPath(); 0754 QFile localBookmarksFile( m_localBookmarksPath ); 0755 QByteArray result = m_downloadReply->readAll(); 0756 QBuffer buffer( &result ); 0757 buffer.open( QIODevice::ReadOnly ); 0758 0759 if( lastSyncedPath.isEmpty() ) { 0760 if( localBookmarksFile.exists() ) { 0761 mDebug() << "Conflict between remote bookmarks and local ones"; 0762 m_diffA = diff( &buffer, m_localBookmarksPath ); 0763 m_diffB = diff( m_localBookmarksPath, &buffer ); 0764 } else { 0765 saveDownloadedToCache( result ); 0766 return; 0767 } 0768 } 0769 else 0770 { 0771 m_diffA = diff( lastSyncedPath, m_localBookmarksPath ); 0772 m_diffB = diff( lastSyncedPath, &buffer ); 0773 } 0774 0775 m_merged.clear(); 0776 merge(); 0777 } 0778 0779 void BookmarkSyncManager::Private::completeMerge() 0780 { 0781 QFile localBookmarksFile( m_localBookmarksPath ); 0782 GeoDataDocument *doc = constructDocument( m_merged ); 0783 GeoWriter writer; 0784 localBookmarksFile.remove(); 0785 localBookmarksFile.open( QFile::ReadWrite ); 0786 writer.write( &localBookmarksFile, doc ); 0787 localBookmarksFile.close(); 0788 uploadBookmarks(); 0789 } 0790 0791 void BookmarkSyncManager::Private::completeUpload() 0792 { 0793 QJsonDocument jsonDoc = QJsonDocument::fromJson(m_uploadReply->readAll()); 0794 QJsonValue dataValue = jsonDoc.object().value(QStringLiteral("data")); 0795 0796 m_cloudTimestamp = dataValue.toString(); 0797 mDebug() << "Uploaded bookmarks to remote server. Timestamp is " << m_cloudTimestamp; 0798 copyLocalToCache(); 0799 emit m_q->syncComplete(); 0800 } 0801 0802 } 0803 0804 #include "moc_BookmarkSyncManager.cpp"