• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MerginMaps / input / 4540060372

pending completion
4540060372

push

github

Unknown Committer
Unknown Commit Message

7947 of 13076 relevant lines covered (60.78%)

106.29 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.23
/core/merginapi.cpp
1
/***************************************************************************
2
 *                                                                         *
3
 *   This program is free software; you can redistribute it and/or modify  *
4
 *   it under the terms of the GNU General Public License as published by  *
5
 *   the Free Software Foundation; either version 2 of the License, or     *
6
 *   (at your option) any later version.                                   *
7
 *                                                                         *
8
 ***************************************************************************/
9

10
#include "merginapi.h"
11

12
#include <QtNetwork>
13
#include <QJsonDocument>
14
#include <QJsonObject>
15
#include <QJsonArray>
16
#include <QDate>
17
#include <QByteArray>
18
#include <QSet>
19
#include <QUuid>
20
#include <QtMath>
21

22
#include "coreutils.h"
23
#include "geodiffutils.h"
24
#include "localprojectsmanager.h"
25

26
#include <geodiff.h>
27

28
const QString MerginApi::sMetadataFile = QStringLiteral( "/.mergin/mergin.json" );
17✔
29
const QString MerginApi::sMetadataFolder = QStringLiteral( ".mergin" );
17✔
30
const QString MerginApi::sMerginConfigFile = QStringLiteral( "mergin-config.json" );
17✔
31
const QString MerginApi::sMarketingPageRoot = QStringLiteral( "https://merginmaps.com/" );
17✔
32
const QString MerginApi::sDefaultApiRoot = QStringLiteral( "https://app.merginmaps.com/" );
17✔
33
const QSet<QString> MerginApi::sIgnoreExtensions = QSet<QString>() << "gpkg-shm" << "gpkg-wal" << "qgs~" << "qgz~" << "pyc" << "swap";
17✔
34
const QSet<QString> MerginApi::sIgnoreImageExtensions = QSet<QString>() << "jpg" << "jpeg" << "png";
17✔
35
const QSet<QString> MerginApi::sIgnoreFiles = QSet<QString>() << "mergin.json" << ".DS_Store";
17✔
36
const int MerginApi::UPLOAD_CHUNK_SIZE = 10 * 1024 * 1024; // Should be the same as on Mergin server
37

38

39
MerginApi::MerginApi( LocalProjectsManager &localProjects, QObject *parent )
88✔
40
  : QObject( parent )
22✔
41
  , mLocalProjects( localProjects )
22✔
42
  , mDataDir( localProjects.dataDir() )
22✔
43
  , mUserInfo( new MerginUserInfo )
22✔
44
  , mWorkspaceInfo( new MerginWorkspaceInfo )
22✔
45
  , mSubscriptionInfo( new MerginSubscriptionInfo )
22✔
46
  , mUserAuth( new MerginUserAuth )
22✔
47
{
66✔
48
  // load cached data if there are any
49
  QSettings cache;
22✔
50
  if ( cache.contains( QStringLiteral( "Input/apiRoot" ) ) )
22✔
51
  {
52
    loadCache();
21✔
53
  }
21✔
54
  else
55
  {
56
    // set default api root
57
    setApiRoot( defaultApiRoot() );
1✔
58
  }
59

60
  qRegisterMetaType<Transactions>();
22✔
61

62
  QObject::connect( this, &MerginApi::authChanged, this, &MerginApi::saveAuthData );
22✔
63
  QObject::connect( this, &MerginApi::apiRootChanged, this, &MerginApi::pingMergin );
22✔
64
  QObject::connect( this, &MerginApi::apiRootChanged, this, &MerginApi::getServerConfig );
22✔
65
  QObject::connect( this, &MerginApi::pingMerginFinished, this, &MerginApi::checkMerginVersion );
22✔
66
  QObject::connect( this, &MerginApi::workspaceCreated, this, &MerginApi::getUserInfo );
22✔
67
  QObject::connect( this, &MerginApi::serverTypeChanged, this, &MerginApi::getUserInfo );
22✔
68
  QObject::connect( this, &MerginApi::processInvitationFinished, this, &MerginApi::getUserInfo );
22✔
69
  QObject::connect( this, &MerginApi::getWorkspaceInfoFinished, this, &MerginApi::getServiceInfo );
22✔
70
  QObject::connect( mUserInfo, &MerginUserInfo::userInfoChanged, this, &MerginApi::userInfoChanged );
22✔
71
  QObject::connect( mUserInfo, &MerginUserInfo::activeWorkspaceChanged, this, &MerginApi::activeWorkspaceChanged );
22✔
72
  QObject::connect( mUserInfo, &MerginUserInfo::activeWorkspaceChanged, this, &MerginApi::getWorkspaceInfo );
22✔
73
  QObject::connect( mUserInfo, &MerginUserInfo::hasWorkspacesChanged, this, &MerginApi::hasWorkspacesChanged );
22✔
74
  QObject::connect( mSubscriptionInfo, &MerginSubscriptionInfo::subscriptionInfoChanged, this, &MerginApi::subscriptionInfoChanged );
22✔
75
  QObject::connect( mSubscriptionInfo, &MerginSubscriptionInfo::planProductIdChanged, this, &MerginApi::onPlanProductIdChanged );
22✔
76
  QObject::connect( mUserAuth, &MerginUserAuth::authChanged, this, &MerginApi::authChanged );
22✔
77
  QObject::connect( mUserAuth, &MerginUserAuth::authChanged, this, [this]()
33✔
78
  {
79
    if ( mUserAuth->hasAuthData() )
11✔
80
    {
81
      // do not call /user/profile when user just logged out
82
      getUserInfo();
9✔
83
    }
9✔
84
  } );
11✔
85

86
  GEODIFF_init();
22✔
87
  GEODIFF_setLoggerCallback( &GeodiffUtils::log );
22✔
88
  GEODIFF_setMaximumLoggerLevel( GEODIFF_LoggerLevel::LevelDebug );
22✔
89

90
  //
91
  // check if the cache is up to date:
92
  //  - server url and type
93
  //  - user auth and info
94
  //  - workspace info
95
  //
96

97
  getServerConfig();
22✔
98
  pingMergin();
22✔
99

100
  if ( mUserAuth->hasAuthData() )
22✔
101
  {
102
    QObject::connect( this, &MerginApi::pingMerginFinished, this, &MerginApi::getUserInfo, Qt::SingleShotConnection );
17✔
103
    QObject::connect( this, &MerginApi::userInfoReplyFinished, this, &MerginApi::getWorkspaceInfo, Qt::SingleShotConnection );
17✔
104
  }
17✔
105
}
44✔
106

107
void MerginApi::loadCache()
21✔
108
{
109
  QSettings settings;
21✔
110
  mApiRoot = settings.value( QStringLiteral( "Input/apiRoot" ) ).toString();
21✔
111
  int serverType = settings.value( QStringLiteral( "Input/serverType" ) ).toInt();
21✔
112

113
  mServerType = static_cast<MerginServerType::ServerType>( serverType );
21✔
114

115
  mUserAuth->loadAuthData();
21✔
116
  mUserInfo->loadWorkspacesData();
21✔
117
}
21✔
118

119
MerginUserAuth *MerginApi::userAuth() const
64✔
120
{
121
  return mUserAuth;
64✔
122
}
123

124
MerginUserInfo *MerginApi::userInfo() const
6✔
125
{
126
  return mUserInfo;
6✔
127
}
128

129
MerginWorkspaceInfo *MerginApi::workspaceInfo() const
×
130
{
131
  return mWorkspaceInfo;
×
132
}
133

134
MerginSubscriptionInfo *MerginApi::subscriptionInfo() const
47✔
135
{
136
  return mSubscriptionInfo;
47✔
137
}
138

139
QString MerginApi::listProjects( const QString &searchExpression, const QString &flag, const int page )
22✔
140
{
141
  bool authorize = flag != "public";
22✔
142

143
  if ( ( authorize && !validateAuth() ) || mApiVersionStatus != MerginApiStatus::OK )
22✔
144
  {
145
    emit listProjectsFailed();
×
146
    return QString();
×
147
  }
148

149
  QUrlQuery query;
22✔
150

151
  if ( flag == "workspace" )
22✔
152
  {
153
    if ( mUserInfo->activeWorkspaceId() < 0 )
×
154
    {
155
      emit listProjectsFailed();
×
156
      return QString();
×
157
    }
158

159
    query.addQueryItem( "only_namespace", mUserInfo->activeWorkspaceName() );
×
160
  }
×
161
  else if ( flag == "created" )
22✔
162
  {
163
    query.addQueryItem( "flag", "created" );
22✔
164
  }
22✔
165
  else if ( flag == "shared" )
×
166
  {
167
    query.addQueryItem( "flag", "shared" );
×
168
  }
×
169
  else if ( flag == "public" )
×
170
  {
171
    query.addQueryItem( "only_public", "true" );
×
172
  }
×
173

174
  if ( !searchExpression.isEmpty() )
22✔
175
  {
176
    query.addQueryItem( "name", searchExpression.toUtf8().toPercentEncoding() );
×
177
  }
×
178

179
  query.addQueryItem( "order_params", QStringLiteral( "namespace_asc,name_asc" ) );
22✔
180

181
  // Required query parameters
182
  query.addQueryItem( "page", QString::number( page ) );
22✔
183
  query.addQueryItem( "per_page", QString::number( PROJECT_PER_PAGE ) );
22✔
184

185
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/paginated" ) );
22✔
186
  url.setQuery( query );
22✔
187

188
  // Even if the authorization is not required, it can be include to fetch more results
189
  QNetworkRequest request = getDefaultRequest( mUserAuth->hasAuthData() );
22✔
190
  request.setUrl( url );
22✔
191

192
  QString requestId = CoreUtils::uuidWithoutBraces( QUuid::createUuid() );
22✔
193

194
  QNetworkReply *reply = mManager.get( request );
22✔
195
  CoreUtils::log( "list projects", QStringLiteral( "Requesting: " ) + url.toString() );
22✔
196
  connect( reply, &QNetworkReply::finished, this, [this, requestId]() {this->listProjectsReplyFinished( requestId );} );
44✔
197

198
  return requestId;
22✔
199
}
22✔
200

201
QString MerginApi::listProjectsByName( const QStringList &projectNames )
7✔
202
{
203
  if ( mApiVersionStatus != MerginApiStatus::OK )
7✔
204
  {
205
    emit listProjectsFailed();
×
206
    return QLatin1String();
×
207
  }
208

209
  // Authentification is optional in this case, as there might be public projects without the need to be logged in.
210
  // We only want to include auth token when user is logged in.
211
  // User's token, however, might have already expired, so let's just refresh it.
212
  refreshAuthToken();
7✔
213

214
  // construct JSON body
215
  QJsonDocument body;
7✔
216
  QJsonObject projects;
7✔
217
  QJsonArray projectsArr = QJsonArray::fromStringList( projectNames );
7✔
218

219
  projects.insert( "projects", projectsArr );
7✔
220
  body.setObject( projects );
7✔
221

222
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/by_names" ) );
7✔
223

224
  QNetworkRequest request = getDefaultRequest( true );
7✔
225
  request.setUrl( url );
7✔
226
  request.setRawHeader( "Content-type", "application/json" );
7✔
227

228
  QString requestId = CoreUtils::uuidWithoutBraces( QUuid::createUuid() );
7✔
229

230
  QNetworkReply *reply = mManager.post( request, body.toJson() );
7✔
231
  CoreUtils::log( "list projects by name", QStringLiteral( "Requesting: " ) + url.toString() );
7✔
232
  connect( reply, &QNetworkReply::finished, this, [this, requestId]() {this->listProjectsByNameReplyFinished( requestId );} );
14✔
233

234
  return requestId;
7✔
235
}
7✔
236

237

238
void MerginApi::downloadNextItem( const QString &projectFullName )
275✔
239
{
240
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
275✔
241
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
275✔
242

243
  if ( transaction.downloadQueue.isEmpty() )
275✔
244
  {
245
    // there's nothing to download so just finalize the pull
246
    finalizeProjectPull( projectFullName );
123✔
247
    return;
123✔
248
  }
249

250
  DownloadQueueItem item = transaction.downloadQueue.takeFirst();
152✔
251

252
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/raw/" ) + projectFullName );
152✔
253
  QUrlQuery query;
152✔
254
  // Handles special chars in a filePath (e.g prevents to convert "+" sign into a space)
255
  query.addQueryItem( "file", item.filePath.toUtf8().toPercentEncoding() );
152✔
256
  query.addQueryItem( "version", QStringLiteral( "v%1" ).arg( item.version ) );
152✔
257
  if ( item.downloadDiff )
152✔
258
    query.addQueryItem( "diff", "true" );
10✔
259
  url.setQuery( query );
152✔
260

261
  QNetworkRequest request = getDefaultRequest();
152✔
262
  request.setUrl( url );
152✔
263
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
152✔
264
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrTempFileName ), item.tempFileName );
152✔
265

266
  QString range;
152✔
267
  if ( item.rangeFrom != -1 && item.rangeTo != -1 )
152✔
268
  {
269
    range = QStringLiteral( "bytes=%1-%2" ).arg( item.rangeFrom ).arg( item.rangeTo );
142✔
270
    request.setRawHeader( "Range", range.toUtf8() );
142✔
271
  }
142✔
272

273
  Q_ASSERT( !transaction.replyPullItem );
152✔
274
  transaction.replyPullItem = mManager.get( request );
152✔
275
  connect( transaction.replyPullItem, &QNetworkReply::finished, this, &MerginApi::downloadItemReplyFinished );
152✔
276

277
  CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Requesting item: " ) + url.toString() +
304✔
278
                  ( !range.isEmpty() ? " Range: " + range : QString() ) );
152✔
279
}
275✔
280

281
void MerginApi::removeProjectsTempFolder( const QString &projectNamespace, const QString &projectName )
62✔
282
{
283
  if ( projectNamespace.isEmpty() || projectName.isEmpty() )
62✔
284
    return; // otherwise we could remove enitre users temp or entire .temp
×
285

286
  QString path = getTempProjectDir( getFullProjectName( projectNamespace, projectName ) );
62✔
287
  QDir( path ).removeRecursively();
62✔
288
}
62✔
289

290
QNetworkRequest MerginApi::getDefaultRequest( bool withAuth )
1,032✔
291
{
292
  QNetworkRequest request;
1,032✔
293
  QString info = CoreUtils::appInfo();
1,032✔
294
  request.setRawHeader( "User-Agent", QByteArray( info.toUtf8() ) );
1,032✔
295
  if ( withAuth )
1,032✔
296
    request.setRawHeader( "Authorization", QByteArray( "Bearer " + mUserAuth->authToken() ) );
993✔
297

298
  return request;
1,032✔
299
}
1,032✔
300

301
bool MerginApi::projectFileHasBeenUpdated( const ProjectDiff &diff )
×
302
{
303
  for ( QString filePath : diff.remoteAdded )
×
304
  {
305
    if ( CoreUtils::hasProjectFileExtension( filePath ) )
×
306
      return true;
×
307
  }
×
308

309
  for ( QString filePath : diff.remoteUpdated )
×
310
  {
311
    if ( CoreUtils::hasProjectFileExtension( filePath ) )
×
312
      return true;
×
313
  }
×
314

315
  return false;
×
316
}
×
317

318
bool MerginApi::supportsSelectiveSync() const
×
319
{
320
  return mSupportsSelectiveSync;
×
321
}
322

323
void MerginApi::setSupportsSelectiveSync( bool supportsSelectiveSync )
4✔
324
{
325
  mSupportsSelectiveSync = supportsSelectiveSync;
4✔
326
}
4✔
327

328
bool MerginApi::apiSupportsSubscriptions() const
22✔
329
{
330
  return mApiSupportsSubscriptions;
22✔
331
}
332

333
void MerginApi::setApiSupportsSubscriptions( bool apiSupportsSubscriptions )
11✔
334
{
335
  if ( mApiSupportsSubscriptions != apiSupportsSubscriptions )
11✔
336
  {
337
    mApiSupportsSubscriptions = apiSupportsSubscriptions;
10✔
338
    emit apiSupportsSubscriptionsChanged();
10✔
339
  }
10✔
340
}
11✔
341

342
#if !defined(USE_MERGIN_DUMMY_API_KEY)
343
#include "merginsecrets.cpp"
344
#endif
345

346
QString MerginApi::getApiKey( const QString &serverName )
3✔
347
{
348
#if defined(USE_MERGIN_DUMMY_API_KEY)
349
  Q_UNUSED( serverName );
350
#else
351
  QString secretKey = __getSecretApiKey( serverName );
3✔
352
  if ( !secretKey.isEmpty() )
3✔
353
    return secretKey;
3✔
354
#endif
355
  return "not-secret-key";
×
356
}
3✔
357

358
void MerginApi::downloadItemReplyFinished()
152✔
359
{
360
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
152✔
361
  Q_ASSERT( r );
152✔
362

363
  QString projectFullName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ) ).toString();
152✔
364
  QString tempFileName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrTempFileName ) ).toString();
152✔
365

366
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
152✔
367
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
152✔
368
  Q_ASSERT( r == transaction.replyPullItem );
152✔
369

370
  if ( r->error() == QNetworkReply::NoError )
152✔
371
  {
372
    QByteArray data = r->readAll();
151✔
373

374
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Downloaded item (%1 bytes)" ).arg( data.size() ) );
151✔
375

376
    QString tempFolder = getTempProjectDir( projectFullName );
151✔
377
    QString tempFilePath = tempFolder + "/" + tempFileName;
151✔
378
    createPathIfNotExists( tempFilePath );
151✔
379

380
    // save to a tmp file, assemble at the end
381
    QFile file( tempFilePath );
151✔
382
    if ( file.open( QIODevice::WriteOnly ) )
151✔
383
    {
384
      file.write( data );
151✔
385
      file.close();
151✔
386
    }
151✔
387
    else
388
    {
389
      CoreUtils::log( "pull " + projectFullName, "Failed to open for writing: " + file.fileName() );
×
390
    }
391

392
    transaction.transferedSize += data.size();
151✔
393
    emit syncProjectStatusChanged( projectFullName, transaction.transferedSize / transaction.totalSize );
151✔
394

395
    transaction.replyPullItem->deleteLater();
151✔
396
    transaction.replyPullItem = nullptr;
151✔
397

398
    // Send another request (or finish)
399
    downloadNextItem( projectFullName );
151✔
400
  }
151✔
401
  else
402
  {
403
    QString serverMsg = extractServerErrorMsg( r->readAll() );
1✔
404
    if ( serverMsg.isEmpty() )
1✔
405
    {
406
      serverMsg = r->errorString();
1✔
407
    }
1✔
408
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) );
1✔
409

410
    transaction.replyPullItem->deleteLater();
1✔
411
    transaction.replyPullItem = nullptr;
1✔
412

413
    // get rid of the temporary download dir where we may have left some downloaded files
414
    QDir( getTempProjectDir( projectFullName ) ).removeRecursively();
1✔
415

416
    if ( transaction.firstTimeDownload )
1✔
417
    {
418
      Q_ASSERT( !transaction.projectDir.isEmpty() );
1✔
419
      QDir( transaction.projectDir ).removeRecursively();
1✔
420
    }
1✔
421

422
    int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
1✔
423
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: downloadFile" ), httpCode, projectFullName );
1✔
424

425
    finishProjectSync( projectFullName, false );
1✔
426
  }
1✔
427
}
152✔
428

429
void MerginApi::cacheServerConfig()
39✔
430
{
431
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
39✔
432
  Q_ASSERT( r );
39✔
433

434
  QString projectFullName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ) ).toString();
39✔
435

436
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
39✔
437
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
39✔
438
  Q_ASSERT( r == transaction.replyPullItem );
39✔
439

440
  if ( r->error() == QNetworkReply::NoError )
39✔
441
  {
442
    QByteArray data = r->readAll();
39✔
443

444
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Downloaded mergin config (%1 bytes)" ).arg( data.size() ) );
39✔
445
    transaction.config = MerginConfig::fromJson( data );
39✔
446

447
    transaction.replyPullItem->deleteLater();
39✔
448
    transaction.replyPullItem = nullptr;
39✔
449

450
    prepareDownloadConfig( projectFullName, true );
39✔
451
  }
39✔
452
  else
453
  {
454
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
455
    if ( serverMsg.isEmpty() )
×
456
    {
457
      serverMsg = r->errorString();
×
458
    }
×
459
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Failed to cache mergin config - %1. %2" ).arg( r->errorString(), serverMsg ) );
×
460

461
    transaction.replyPullItem->deleteLater();
×
462
    transaction.replyPullItem = nullptr;
×
463

464
    // get rid of the temporary download dir where we may have left some downloaded files
465
    CoreUtils::removeDir( getTempProjectDir( projectFullName ) );
×
466

467
    if ( transaction.firstTimeDownload )
×
468
    {
469
      CoreUtils::removeDir( transaction.projectDir );
×
470
    }
×
471

472
    int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
×
473
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: downloadFile" ), httpCode, projectFullName );
×
474

475
    finishProjectSync( projectFullName, false );
×
476
  }
×
477
}
39✔
478

479

480
void MerginApi::pushFile( const QString &projectFullName, const QString &transactionUUID, MerginFile file, int chunkNo )
152✔
481
{
482
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
152✔
483
  {
484
    return;
×
485
  }
486

487
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
152✔
488
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
152✔
489

490
  QString chunkID = file.chunks.at( chunkNo );
152✔
491

492
  QString filePath;
152✔
493
  if ( file.diffName.isEmpty() )
152✔
494
    filePath = transaction.projectDir + "/" + file.path;
140✔
495
  else  // use diff file instead of full file
496
    filePath = transaction.projectDir + "/.mergin/" + file.diffName;
12✔
497

498
  QFile f( filePath );
152✔
499
  QByteArray data;
152✔
500

501
  if ( f.open( QIODevice::ReadOnly ) )
152✔
502
  {
503
    f.seek( chunkNo * UPLOAD_CHUNK_SIZE );
152✔
504
    data = f.read( UPLOAD_CHUNK_SIZE );
152✔
505
  }
152✔
506

507
  QNetworkRequest request = getDefaultRequest();
152✔
508
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/push/chunk/%1/%2" ).arg( transactionUUID, chunkID ) );
152✔
509
  request.setUrl( url );
152✔
510
  request.setRawHeader( "Content-Type", "application/octet-stream" );
152✔
511
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
152✔
512

513
  Q_ASSERT( !transaction.replyPushFile );
152✔
514
  transaction.replyPushFile = mManager.post( request, data );
152✔
515
  connect( transaction.replyPushFile, &QNetworkReply::finished, this, &MerginApi::pushFileReplyFinished );
152✔
516

517
  CoreUtils::log( "push " + projectFullName, QStringLiteral( "Uploading item: " ) + url.toString() );
152✔
518
}
152✔
519

520
void MerginApi::pushStart( const QString &projectFullName, const QByteArray &json )
115✔
521
{
522
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
115✔
523
  {
524
    return;
×
525
  }
526

527
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
115✔
528
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
115✔
529

530
  QNetworkRequest request = getDefaultRequest();
115✔
531
  QUrl url( mApiRoot + QStringLiteral( "v1/project/push/%1" ).arg( projectFullName ) );
115✔
532
  request.setUrl( url );
115✔
533
  request.setRawHeader( "Content-Type", "application/json" );
115✔
534
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
115✔
535

536
  Q_ASSERT( !transaction.replyPushStart );
115✔
537
  transaction.replyPushStart = mManager.post( request, json );
115✔
538
  connect( transaction.replyPushStart, &QNetworkReply::finished, this, &MerginApi::pushStartReplyFinished );
115✔
539

540
  CoreUtils::log( "push " + projectFullName, QStringLiteral( "Starting push request: " ) + url.toString() );
115✔
541
}
115✔
542

543
void MerginApi::cancelPush( const QString &projectFullName )
2✔
544
{
545
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
2✔
546
  {
547
    return;
×
548
  }
549

550
  if ( !mTransactionalStatus.contains( projectFullName ) )
2✔
551
    return;
×
552

553
  CoreUtils::log( "push " + projectFullName, QStringLiteral( "User requested cancel" ) );
2✔
554

555
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
2✔
556

557
  // There is an open transaction, abort it followed by calling cancelUpload again.
558
  if ( transaction.replyPushProjectInfo )
2✔
559
  {
560
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Aborting project info request" ) );
1✔
561
    transaction.replyPushProjectInfo->abort();  // will trigger uploadInfoReplyFinished slot and emit sync finished
1✔
562
  }
1✔
563
  else if ( transaction.replyPushStart )
1✔
564
  {
565
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Aborting upload start" ) );
×
566
    transaction.replyPushStart->abort();  // will trigger uploadStartReplyFinished slot and emit sync finished
×
567
  }
×
568
  else if ( transaction.replyPushFile )
1✔
569
  {
570
    QString transactionUUID = transaction.transactionUUID;  // copy transaction uuid as the transaction object will be gone after abort
1✔
571
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Aborting upload file" ) );
1✔
572
    transaction.replyPushFile->abort();  // will trigger pushFileReplyFinished slot and emit sync finished
1✔
573

574
    // also need to cancel the transaction
575
    sendPushCancelRequest( projectFullName, transactionUUID );
1✔
576
  }
1✔
577
  else if ( transaction.replyPushFinish )
×
578
  {
579
    QString transactionUUID = transaction.transactionUUID;  // copy transaction uuid as the transaction object will be gone after abort
×
580
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Aborting upload finish" ) );
×
581
    transaction.replyPushFinish->abort();  // will trigger pushFinishReplyFinished slot and emit sync finished
×
582

583
    sendPushCancelRequest( projectFullName, transactionUUID );
×
584
  }
×
585
  else
586
  {
587
    Q_ASSERT( false );  // unexpected state
×
588
  }
589
}
2✔
590

591

592
void MerginApi::sendPushCancelRequest( const QString &projectFullName, const QString &transactionUUID )
1✔
593
{
594
  QNetworkRequest request = getDefaultRequest();
1✔
595
  QUrl url( mApiRoot + QStringLiteral( "v1/project/push/cancel/%1" ).arg( transactionUUID ) );
1✔
596
  request.setUrl( url );
1✔
597
  request.setRawHeader( "Content-Type", "application/json" );
1✔
598
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
1✔
599

600
  QNetworkReply *reply = mManager.post( request, QByteArray() );
1✔
601
  connect( reply, &QNetworkReply::finished, this, &MerginApi::pushCancelReplyFinished );
1✔
602
  CoreUtils::log( "push " + projectFullName, QStringLiteral( "Requesting upload transaction cancel: " ) + url.toString() );
1✔
603
}
1✔
604

605
void MerginApi::cancelPull( const QString &projectFullName )
2✔
606
{
607
  if ( !mTransactionalStatus.contains( projectFullName ) )
2✔
608
    return;
×
609

610
  CoreUtils::log( "pull " + projectFullName, QStringLiteral( "User requested cancel" ) );
2✔
611

612
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
2✔
613

614
  if ( transaction.replyPullProjectInfo )
2✔
615
  {
616
    // we're still fetching project info
617
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Aborting project info request" ) );
1✔
618
    transaction.replyPullProjectInfo->abort();  // abort will trigger pullInfoReplyFinished() slot
1✔
619
  }
1✔
620
  else if ( transaction.replyPullItem )
1✔
621
  {
622
    // we're already downloading some files
623
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Aborting pending download" ) );
1✔
624
    transaction.replyPullItem->abort();  // abort will trigger downloadItemReplyFinished slot
1✔
625
  }
1✔
626
  else
627
  {
628
    Q_ASSERT( false );  // unexpected state
×
629
  }
630
}
2✔
631

632
void MerginApi::pushFinish( const QString &projectFullName, const QString &transactionUUID )
110✔
633
{
634
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
110✔
635
  {
636
    return;
×
637
  }
638

639
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
110✔
640
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
110✔
641

642
  QNetworkRequest request = getDefaultRequest();
110✔
643
  QUrl url( mApiRoot + QStringLiteral( "v1/project/push/finish/%1" ).arg( transactionUUID ) );
110✔
644
  request.setUrl( url );
110✔
645
  request.setRawHeader( "Content-Type", "application/json" );
110✔
646
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
110✔
647

648
  Q_ASSERT( !transaction.replyPushFinish );
110✔
649
  transaction.replyPushFinish = mManager.post( request, QByteArray() );
110✔
650
  connect( transaction.replyPushFinish, &QNetworkReply::finished, this, &MerginApi::pushFinishReplyFinished );
110✔
651

652
  CoreUtils::log( "push " + projectFullName, QStringLiteral( "Requesting transaction finish: " ) + transactionUUID );
110✔
653
}
110✔
654

655
bool MerginApi::pullProject( const QString &projectNamespace, const QString &projectName, bool withAuth )
100✔
656
{
657
  QString projectFullName = getFullProjectName( projectNamespace, projectName );
100✔
658
  bool pullHasStarted = false;
100✔
659

660
  CoreUtils::log( "pull " + projectFullName, "### Starting ###" );
100✔
661

662
  QNetworkReply *reply = getProjectInfo( projectFullName, withAuth );
100✔
663
  if ( reply )
100✔
664
  {
665
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Requesting project info: " ) + reply->request().url().toString() );
100✔
666

667
    Q_ASSERT( !mTransactionalStatus.contains( projectFullName ) );
100✔
668
    mTransactionalStatus.insert( projectFullName, TransactionStatus() );
100✔
669
    mTransactionalStatus[projectFullName].replyPullProjectInfo = reply;
100✔
670
    mTransactionalStatus[projectFullName].configAllowed = mSupportsSelectiveSync;
100✔
671
    mTransactionalStatus[projectFullName].type = TransactionStatus::Pull;
100✔
672

673
    emit syncProjectStatusChanged( projectFullName, 0 );
100✔
674

675
    connect( reply, &QNetworkReply::finished, this, &MerginApi::pullInfoReplyFinished );
100✔
676
    pullHasStarted = true;
100✔
677
  }
100✔
678
  else
679
  {
680
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "FAILED to create project info request!" ) );
×
681
  }
682

683
  return pullHasStarted;
100✔
684
}
100✔
685

686
bool MerginApi::pushProject( const QString &projectNamespace, const QString &projectName, bool isInitialPush )
143✔
687
{
688
  QString projectFullName = getFullProjectName( projectNamespace, projectName );
143✔
689
  bool pushHasStarted = false;
143✔
690

691
  CoreUtils::log( "push " + projectFullName, "### Starting ###" );
143✔
692

693
  QNetworkReply *reply = getProjectInfo( projectFullName );
143✔
694
  if ( reply )
143✔
695
  {
696
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Requesting project info: " ) + reply->request().url().toString() );
143✔
697

698
    // create entry about pending upload for the project
699
    Q_ASSERT( !mTransactionalStatus.contains( projectFullName ) );
143✔
700
    mTransactionalStatus.insert( projectFullName, TransactionStatus() );
143✔
701
    mTransactionalStatus[projectFullName].replyPushProjectInfo = reply;
143✔
702
    mTransactionalStatus[projectFullName].isInitialPush = isInitialPush;
143✔
703
    mTransactionalStatus[projectFullName].configAllowed = mSupportsSelectiveSync;
143✔
704
    mTransactionalStatus[projectFullName].type = TransactionStatus::Push;
143✔
705

706
    emit syncProjectStatusChanged( projectFullName, 0 );
143✔
707

708
    connect( reply, &QNetworkReply::finished, this, &MerginApi::pushInfoReplyFinished );
143✔
709
    pushHasStarted = true;
143✔
710
  }
143✔
711
  else
712
  {
713
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED to create project info request!" ) );
×
714
  }
715

716
  return pushHasStarted;
143✔
717
}
143✔
718

719
void MerginApi::authorize( const QString &login, const QString &password )
8✔
720
{
721
  if ( login.isEmpty() || password.isEmpty() )
8✔
722
  {
723
    emit authFailed();
×
724
    emit notify( QStringLiteral( "Please enter your login details" ) );
×
725
    return;
×
726
  }
727

728
  mUserAuth->blockSignals( true );
8✔
729
  mUserAuth->setPassword( password );
8✔
730
  mUserAuth->blockSignals( false );
8✔
731

732
  QNetworkRequest request = getDefaultRequest( false );
8✔
733
  QString urlString = mApiRoot + QStringLiteral( "v1/auth/login" );
8✔
734
  QUrl url( urlString );
8✔
735
  request.setUrl( url );
8✔
736
  request.setRawHeader( "Content-Type", "application/json" );
8✔
737

22✔
738
  QJsonDocument jsonDoc;
30✔
739
  QJsonObject jsonObject;
30✔
740
  jsonObject.insert( QStringLiteral( "login" ), login );
8✔
741
  jsonObject.insert( QStringLiteral( "password" ), mUserAuth->password() );
8✔
742
  jsonDoc.setObject( jsonObject );
8✔
743
  QByteArray json = jsonDoc.toJson( QJsonDocument::Compact );
30✔
744

22✔
745
  QNetworkReply *reply = mManager.post( request, json );
8✔
746
  connect( reply, &QNetworkReply::finished, this, &MerginApi::authorizeFinished );
8✔
747
  CoreUtils::log( "auth", QStringLiteral( "Requesting authorization: " ) + url.toString() );
8✔
748
}
8✔
749

22✔
750
void MerginApi::registerUser( const QString &username,
3✔
751
                              const QString &email,
752
                              const QString &password,
753
                              const QString &confirmPassword,
754
                              bool acceptedTOC )
755
{
756
  // Some very basic checks, so we do not validate everything
757
  if ( username.isEmpty() || username.length() < 4 )
3✔
758
  {
759
    emit registrationFailed();
×
760
    emit notify( tr( "Username must have at least 4 characters" ) );
×
761
    return;
×
762
  }
763

764
  if ( !CoreUtils::isValidName( username ) )
3✔
765
  {
766
    emit registrationFailed();
×
767
    emit notify( tr( "Username contains invalid characters" ) );
×
768
    return;
×
769
  }
770

771
  if ( email.isEmpty() || !email.contains( '@' ) || !email.contains( '.' ) )
3✔
772
  {
773
    emit registrationFailed();
×
774
    emit notify( tr( "Please enter a valid email" ) );
×
775
    return;
×
776
  }
777

778
  if ( password.isEmpty() || password.length() < 8 )
3✔
779
  {
780
    emit registrationFailed();
×
781
    QString msg = tr( "Password not strong enough. It must"
×
782
                      "%1 be at least 8 characters long"
783
                      "%1 contain lowercase characters"
784
                      "%1 contain uppercase characters"
785
                      "%1 contain digits or special characters" )
786
                  .arg( "<br />  -" );
×
787
    emit notify( msg );
×
788
    return;
789
  }
×
790

791
  if ( confirmPassword != password )
3✔
792
  {
793
    emit registrationFailed();
×
794
    emit notify( tr( "Passwords do not match" ) );
×
795
    return;
×
796
  }
797

798
  if ( !acceptedTOC )
3✔
799
  {
800
    emit registrationFailed();
×
801
    emit notify( tr( "Please accept Terms and Privacy Policy" ) );
×
802
    return;
×
803
  }
804

805
  // request
806
  QNetworkRequest request = getDefaultRequest( false );
3✔
807
  QString urlString = mApiRoot + QStringLiteral( "v1/auth/register" );
3✔
808
  QUrl url( urlString );
3✔
809
  request.setUrl( url );
3✔
810
  request.setRawHeader( "Content-Type", "application/json" );
3✔
811

812
  QJsonDocument jsonDoc;
3✔
813
  QJsonObject jsonObject;
3✔
814
  jsonObject.insert( QStringLiteral( "username" ), username );
3✔
815
  jsonObject.insert( QStringLiteral( "email" ), email );
3✔
816
  jsonObject.insert( QStringLiteral( "password" ), password );
3✔
817
  jsonObject.insert( QStringLiteral( "api_key" ), getApiKey( mApiRoot ) );
3✔
818
  jsonDoc.setObject( jsonObject );
3✔
819
  QByteArray json = jsonDoc.toJson( QJsonDocument::Compact );
3✔
820
  QNetworkReply *reply = mManager.post( request, json );
3✔
821
  connect( reply, &QNetworkReply::finished, this, [ = ]() { this->registrationFinished( username, password ); } );
6✔
822
  CoreUtils::log( "auth", QStringLiteral( "Requesting registration: " ) + url.toString() );
3✔
823
}
3✔
824

825
void MerginApi::getUserInfo()
26✔
826
{
827
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
26✔
828
  {
829
    return;
6✔
830
  }
831

832
  QString urlString;
20✔
833
  if ( mServerType == MerginServerType::OLD )
20✔
834
  {
835
    urlString = mApiRoot + QStringLiteral( "v1/user/%1" ).arg( mUserAuth->username() );
×
836
  }
×
837
  else
838
  {
839
    urlString = mApiRoot + QStringLiteral( "v1/user/profile" );
20✔
840
  }
841

842
  QNetworkRequest request = getDefaultRequest();
20✔
843
  QUrl url( urlString );
20✔
844
  request.setUrl( url );
20✔
845

846
  QNetworkReply *reply = mManager.get( request );
20✔
847
  CoreUtils::log( "user info", QStringLiteral( "Requesting user info: " ) + url.toString() );
20✔
848
  connect( reply, &QNetworkReply::finished, this, &MerginApi::getUserInfoFinished );
20✔
849
}
26✔
850

851
void MerginApi::getWorkspaceInfo()
11✔
852
{
853
  if ( mServerType == MerginServerType::OLD )
11✔
854
  {
855
    return;
×
856
  }
857

858
  if ( mUserInfo->activeWorkspaceId() == -1 )
11✔
859
  {
860
    return;
×
861
  }
862

863
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
11✔
864
  {
865
    return;
×
866
  }
867

868
  QString urlString = mApiRoot + QStringLiteral( "v1/workspace/%1" ).arg( mUserInfo->activeWorkspaceId() );
11✔
869
  QNetworkRequest request = getDefaultRequest();
11✔
870
  QUrl url( urlString );
11✔
871
  request.setUrl( url );
11✔
872

873
  QNetworkReply *reply = mManager.get( request );
11✔
874
  CoreUtils::log( "workspace info", QStringLiteral( "Requesting workspace info: " ) + url.toString() );
11✔
875
  connect( reply, &QNetworkReply::finished, this, &MerginApi::getWorkspaceInfoReplyFinished );
11✔
876
}
11✔
877

878
void MerginApi::getServiceInfo()
11✔
879
{
880
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
11✔
881
  {
882
    return;
×
883
  }
884

885
  QString urlString;
11✔
886

887
  if ( mServerType == MerginServerType::SAAS )
11✔
888
  {
889
    urlString = mApiRoot + QStringLiteral( "v1/workspace/%1/service" ).arg( mUserInfo->activeWorkspaceId() );
11✔
890
  }
11✔
891
  else if ( mServerType == MerginServerType::OLD )
×
892
  {
893
    urlString = mApiRoot + QStringLiteral( "v1/user/service" );
×
894
  }
×
895
  else
896
  {
897
    return;
×
898
  }
899

900
  QNetworkRequest request = getDefaultRequest( true );
11✔
901
  QUrl url( urlString );
11✔
902
  request.setUrl( url );
11✔
903

904
  QNetworkReply *reply = mManager.get( request );
11✔
905

906
  connect( reply, &QNetworkReply::finished, this, &MerginApi::getServiceInfoReplyFinished );
11✔
907

908
  CoreUtils::log( "Service info", QStringLiteral( "Requesting service info: " ) + url.toString() );
11✔
909
}
11✔
910

911
void MerginApi::getServiceInfoReplyFinished()
10✔
912
{
913
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
10✔
914
  Q_ASSERT( r );
10✔
915

916
  if ( r->error() == QNetworkReply::NoError )
10✔
917
  {
918
    CoreUtils::log( "Service info", QStringLiteral( "Success" ) );
10✔
919

920
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
10✔
921
    if ( doc.isObject() )
10✔
922
    {
923
      QJsonObject docObj = doc.object();
10✔
924
      mSubscriptionInfo->setFromJson( docObj );
10✔
925
    }
10✔
926
  }
10✔
927
  else
928
  {
929
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
930
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "getServiceInfo" ), r->errorString(), serverMsg );
×
931
    CoreUtils::log( "Service info", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
932

933
    mSubscriptionInfo->clear();
×
934

935
    int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
×
936
    if ( httpCode == 404 )
×
937
    {
938
      // no such API on the server, do not emit anything
939
    }
×
940
    else if ( httpCode == 403 )
×
941
    {
942
      // forbidden - I do not have enough rights to see this, do not emit anything
943
    }
×
944
    else
945
    {
946
      emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: getServiceInfo" ) );
×
947
    }
948
  }
×
949

950
  r->deleteLater();
10✔
951
}
10✔
952

953
void MerginApi::clearAuth()
2✔
954
{
955
  mUserAuth->clear();
2✔
956
  mUserInfo->clear();
2✔
957
  mUserInfo->clearCachedWorkspacesInfo();
2✔
958
  mWorkspaceInfo->clear();
2✔
959
  mSubscriptionInfo->clear();
2✔
960
}
2✔
961

962
void MerginApi::resetApiRoot()
×
963
{
964
  QSettings settings;
×
965
  settings.beginGroup( QStringLiteral( "Input/" ) );
×
966
  setApiRoot( defaultApiRoot() );
×
967
  settings.endGroup();
×
968
}
×
969

970
QString MerginApi::resetPasswordUrl()
×
971
{
972
  if ( !mApiRoot.isEmpty() )
×
973
  {
974
    QUrl base( mApiRoot );
×
975
    return base.resolved( QUrl( "login/reset" ) ).toString();
×
976
  }
×
977
  return QString();
×
978
}
×
979

980
bool MerginApi::createProject( const QString &projectNamespace, const QString &projectName, bool isPublic )
40✔
981
{
982
  if ( !validateAuth() )
40✔
983
  {
984
    emit missingAuthorizationError( projectName );
×
985
    return false;
×
986
  }
987

988
  if ( mApiVersionStatus != MerginApiStatus::OK )
40✔
989
  {
990
    return false;
×
991
  }
992

993
  QString projectFullName = getFullProjectName( projectNamespace, projectName );
40✔
994

995
  QNetworkRequest request = getDefaultRequest();
40✔
996
  QUrl url( mApiRoot + QString( "/v1/project/%1" ).arg( projectNamespace ) );
40✔
997
  request.setUrl( url );
40✔
998
  request.setRawHeader( "Content-Type", "application/json" );
40✔
999
  request.setRawHeader( "Accept", "application/json" );
40✔
1000
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
40✔
1001

1002
  QJsonDocument jsonDoc;
40✔
1003
  QJsonObject jsonObject;
40✔
1004
  jsonObject.insert( QStringLiteral( "name" ), projectName );
40✔
1005
  jsonObject.insert( QStringLiteral( "public" ), isPublic );
40✔
1006
  jsonDoc.setObject( jsonObject );
40✔
1007
  QByteArray json = jsonDoc.toJson( QJsonDocument::Compact );
40✔
1008

1009
  QNetworkReply *reply = mManager.post( request, json );
40✔
1010
  connect( reply, &QNetworkReply::finished, this, &MerginApi::createProjectFinished );
40✔
1011
  CoreUtils::log( "create " + projectFullName, QStringLiteral( "Requesting project creation: " ) + url.toString() );
40✔
1012

1013
  return true;
40✔
1014
}
40✔
1015

1016
void MerginApi::deleteProject( const QString &projectNamespace, const QString &projectName, bool informUser )
43✔
1017
{
1018
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
43✔
1019
  {
1020
    return;
×
1021
  }
1022

1023
  QString projectFullName = getFullProjectName( projectNamespace, projectName );
43✔
1024

1025
  QNetworkRequest request = getDefaultRequest();
43✔
1026
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/%1" ).arg( projectFullName ) );
43✔
1027
  request.setUrl( url );
43✔
1028
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
43✔
1029
  QNetworkReply *reply = mManager.deleteResource( request );
43✔
1030
  connect( reply, &QNetworkReply::finished, this, [this, informUser]() { this->deleteProjectFinished( informUser );} );
86✔
1031
  CoreUtils::log( "delete " + projectFullName, QStringLiteral( "Requesting project deletion: " ) + url.toString() );
43✔
1032
}
43✔
1033

1034
void MerginApi::saveAuthData()
11✔
1035
{
1036
  QSettings settings;
11✔
1037
  settings.beginGroup( "Input/" );
11✔
1038
  settings.setValue( "apiRoot", mApiRoot );
11✔
1039
  settings.endGroup();
11✔
1040

1041
  mUserAuth->saveAuthData();
11✔
1042
  mUserInfo->clear();
11✔
1043
}
11✔
1044

1045
void MerginApi::createProjectFinished()
40✔
1046
{
1047
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
40✔
1048
  Q_ASSERT( r );
40✔
1049

1050
  QString projectFullName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ) ).toString();
40✔
1051

1052
  QString projectNamespace, projectName;
40✔
1053
  extractProjectName( projectFullName, projectNamespace, projectName );
40✔
1054

1055
  if ( r->error() == QNetworkReply::NoError )
40✔
1056
  {
1057
    CoreUtils::log( "create " + projectFullName, QStringLiteral( "Success" ) );
39✔
1058
    emit projectCreated( projectFullName, true );
39✔
1059

1060

1061
    // Upload data if createProject has been called for a local project with empty namespace (case of migrating a project)
1062
    for ( const LocalProject &info : mLocalProjects.projects() )
433✔
1063
    {
1064
      if ( info.projectName == projectName && info.projectNamespace.isEmpty() )
394✔
1065
      {
1066
        mLocalProjects.updateNamespace( info.projectDir, projectNamespace );
3✔
1067
        emit projectAttachedToMergin( projectFullName, projectName );
3✔
1068

1069
        QDir projectDir( info.projectDir );
3✔
1070
        if ( projectDir.exists() && !projectDir.isEmpty() )
3✔
1071
        {
1072
          pushProject( projectNamespace, projectName, true );
3✔
1073
        }
3✔
1074
      }
3✔
1075
    }
1076
  }
39✔
1077
  else
1078
  {
1079
    QString serverMsg = extractServerErrorMsg( r->readAll() );
1✔
1080
    QString message = QStringLiteral( "FAILED - %1: %2" ).arg( r->errorString(), serverMsg );
1✔
1081
    CoreUtils::log( "create " + projectFullName, message );
1✔
1082

1083
    int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
1✔
1084

1085
    emit projectCreated( projectFullName, false );
1✔
1086
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: createProject" ), httpCode, projectName );
1✔
1087
  }
1✔
1088
  r->deleteLater();
40✔
1089
}
40✔
1090

1091
void MerginApi::deleteProjectFinished( bool informUser )
43✔
1092
{
1093
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
43✔
1094
  Q_ASSERT( r );
43✔
1095

1096
  QString projectFullName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ) ).toString();
43✔
1097

1098
  if ( r->error() == QNetworkReply::NoError )
43✔
1099
  {
1100
    CoreUtils::log( "delete " + projectFullName, QStringLiteral( "Success" ) );
2✔
1101

1102
    if ( informUser )
2✔
1103
      emit notify( QStringLiteral( "Project deleted" ) );
2✔
1104

1105
    emit serverProjectDeleted( projectFullName, true );
2✔
1106
  }
2✔
1107
  else
1108
  {
1109
    QString serverMsg = extractServerErrorMsg( r->readAll() );
41✔
1110
    CoreUtils::log( "delete " + projectFullName, QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) );
41✔
1111
    emit serverProjectDeleted( projectFullName, false );
41✔
1112
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: deleteProject" ) );
41✔
1113
  }
41✔
1114
  r->deleteLater();
43✔
1115
}
43✔
1116

1117
void MerginApi::authorizeFinished()
8✔
1118
{
1119
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
8✔
1120
  Q_ASSERT( r );
8✔
1121

1122
  if ( r->error() == QNetworkReply::NoError )
8✔
1123
  {
1124
    CoreUtils::log( "auth", QStringLiteral( "Success" ) );
8✔
1125
    const QByteArray data = r->readAll();
8✔
1126
    QJsonDocument doc = QJsonDocument::fromJson( data );
8✔
1127
    if ( doc.isObject() )
8✔
1128
    {
1129
      QJsonObject docObj = doc.object();
8✔
1130
      mUserAuth->setFromJson( docObj );
8✔
1131
    }
8✔
1132
    else
1133
    {
1134
      mUserAuth->blockSignals( true );
×
1135
      mUserAuth->setUsername( QString() ); //clearTokenData emits the authChanged
×
1136
      mUserAuth->setPassword( QString() ); //clearTokenData emits the authChanged
×
1137
      mUserAuth->blockSignals( false );
×
1138

1139
      mUserAuth->clearTokenData();
×
1140
      emit authFailed();
×
1141
      CoreUtils::log( "auth", QStringLiteral( "FAILED - invalid JSON response" ) );
×
1142
      emit notify( "Internal server error during authorization" );
×
1143
    }
1144
  }
8✔
1145
  else
1146
  {
1147
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
1148
    CoreUtils::log( "auth", QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) );
×
1149
    QVariant statusCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute );
×
1150
    int status = statusCode.toInt();
×
1151
    if ( status == 401 || status == 400 )
×
1152
    {
1153
      emit authFailed();
×
1154
      emit notify( serverMsg );
×
1155
    }
×
1156
    else
1157
    {
1158
      emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: authorize" ) );
×
1159
    }
1160
    mUserAuth->blockSignals( true );
×
1161
    mUserAuth->setUsername( QString() );
×
1162
    mUserAuth->setPassword( QString() );
×
1163
    mUserAuth->blockSignals( false );
×
1164
    mUserAuth->clearTokenData();
×
1165
  }
×
1166
  if ( mAuthLoopEvent.isRunning() )
8✔
1167
  {
1168
    mAuthLoopEvent.exit();
1✔
1169
  }
1✔
1170
  r->deleteLater();
8✔
1171
}
8✔
1172

1173
void MerginApi::registrationFinished( const QString &username, const QString &password )
3✔
1174
{
1175
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
3✔
1176
  Q_ASSERT( r );
3✔
1177

1178
  if ( r->error() == QNetworkReply::NoError )
3✔
1179
  {
1180
    CoreUtils::log( "register", QStringLiteral( "Success" ) );
3✔
1181
    QString msg = tr( "Registration successful" );
3✔
1182
    emit notify( msg );
3✔
1183

1184
    if ( !username.isEmpty() && !password.isEmpty() ) // log in immediately
3✔
1185
      authorize( username, password );
3✔
1186

1187
    emit registrationSucceeded();
3✔
1188
  }
3✔
1189
  else
1190
  {
1191
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
1192
    CoreUtils::log( "register", QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) );
×
1193
    QVariant statusCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute );
×
1194
    int status = statusCode.toInt();
×
1195
    if ( status == 401 || status == 400 )
×
1196
    {
1197
      emit registrationFailed();
×
1198
      emit notify( serverMsg );
×
1199
    }
×
1200
    else if ( status == 404 )
×
1201
    {
1202
      // the self-registration is not allowed on the server
1203
      emit registrationFailed();
×
1204
      emit notify( tr( "New registrations are not allowed on the selected Mergin server.%1Please check with your administrator." ).arg( "\n" ) );
×
1205
    }
×
1206
    else
1207
    {
1208
      emit registrationFailed();
×
1209
      emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: register" ) );
×
1210
    }
1211
  }
×
1212
  r->deleteLater();
3✔
1213
}
3✔
1214

1215
void MerginApi::pingMerginReplyFinished()
11✔
1216
{
1217
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
11✔
1218
  Q_ASSERT( r );
11✔
1219
  QString apiVersion;
11✔
1220
  QString serverMsg;
11✔
1221
  bool serverSupportsSubscriptions = false;
11✔
1222

1223
  if ( r->error() == QNetworkReply::NoError )
11✔
1224
  {
1225
    CoreUtils::log( "ping", QStringLiteral( "Success" ) );
11✔
1226
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
11✔
1227
    if ( doc.isObject() )
11✔
1228
    {
1229
      QJsonObject obj = doc.object();
11✔
1230
      apiVersion = obj.value( QStringLiteral( "version" ) ).toString();
11✔
1231
      serverSupportsSubscriptions = obj.value( QStringLiteral( "subscriptions_enabled" ) ).toBool();
11✔
1232
    }
11✔
1233
  }
11✔
1234
  else
1235
  {
1236
    serverMsg = extractServerErrorMsg( r->readAll() );
×
1237
    CoreUtils::log( "ping", QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) );
×
1238
  }
1239
  r->deleteLater();
11✔
1240
  emit pingMerginFinished( apiVersion, serverSupportsSubscriptions, serverMsg );
11✔
1241
}
11✔
1242

1243
void MerginApi::onPlanProductIdChanged()
×
1244
{
1245
  if ( mUserAuth->hasAuthData() )
×
1246
  {
1247
    if ( mServerType == MerginServerType::OLD )
×
1248
    {
1249
      getUserInfo();
×
1250
    }
×
1251
    else
1252
    {
1253
      getWorkspaceInfo();
×
1254
    }
1255
  }
×
1256
}
×
1257

1258
QNetworkReply *MerginApi::getProjectInfo( const QString &projectFullName, bool withAuth )
243✔
1259
{
1260
  if ( withAuth && !validateAuth() )
243✔
1261
  {
1262
    emit missingAuthorizationError( projectFullName );
×
1263
    return nullptr;
×
1264
  }
1265

1266
  if ( mApiVersionStatus != MerginApiStatus::OK )
243✔
1267
  {
1268
    return nullptr;
×
1269
  }
1270

1271
  int sinceVersion = -1;
243✔
1272
  LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName );
243✔
1273
  if ( projectInfo.isValid() )
243✔
1274
  {
1275
    // let's also fetch the recent history of diffable files
1276
    // (the "since" is inclusive, so if we are on v2, we want to use since=v3 which will include v2->v3, v3->v4, ...)
1277
    sinceVersion = projectInfo.localVersion + 1;
180✔
1278
  }
180✔
1279

1280
  QUrlQuery query;
243✔
1281
  if ( sinceVersion != -1 )
243✔
1282
    query.addQueryItem( QStringLiteral( "since" ), QStringLiteral( "v%1" ).arg( sinceVersion ) );
180✔
1283

1284
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/%1" ).arg( projectFullName ) );
243✔
1285
  url.setQuery( query );
243✔
1286

1287
  QNetworkRequest request = getDefaultRequest( withAuth );
243✔
1288
  request.setUrl( url );
243✔
1289
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
243✔
1290

1291
  return mManager.get( request );
243✔
1292
}
243✔
1293

1294
bool MerginApi::validateAuth()
778✔
1295
{
1296
  if ( !mUserAuth->hasAuthData() )
778✔
1297
  {
1298
    emit authRequested();
6✔
1299
    return false;
6✔
1300
  }
1301

1302
  if ( mUserAuth->authToken().isEmpty() || mUserAuth->tokenExpiration() < QDateTime().currentDateTime().toUTC() )
772✔
1303
  {
1304
    authorize( mUserAuth->username(), mUserAuth->password() );
1✔
1305
    CoreUtils::log( QStringLiteral( "MerginApi" ), QStringLiteral( "Requesting authorization because of missing or expired token." ) );
1✔
1306
    mAuthLoopEvent.exec();
1✔
1307
  }
1✔
1308
  return true;
772✔
1309
}
778✔
1310

1311
void MerginApi::checkMerginVersion( QString apiVersion, bool serverSupportsSubscriptions, QString msg )
11✔
1312
{
1313
  setApiSupportsSubscriptions( serverSupportsSubscriptions );
11✔
1314

1315
  if ( msg.isEmpty() )
11✔
1316
  {
1317
    int major = -1;
11✔
1318
    int minor = -1;
11✔
1319
    QRegularExpression re;
11✔
1320
    re.setPattern( QStringLiteral( "(?<major>\\d+)[.](?<minor>\\d+)" ) );
11✔
1321
    QRegularExpressionMatch match = re.match( apiVersion );
11✔
1322
    if ( match.hasMatch() )
11✔
1323
    {
1324
      major = match.captured( "major" ).toInt();
11✔
1325
      minor = match.captured( "minor" ).toInt();
11✔
1326
    }
11✔
1327

1328
    if ( ( MERGIN_API_VERSION_MAJOR == major && MERGIN_API_VERSION_MINOR <= minor ) || ( MERGIN_API_VERSION_MAJOR < major ) )
11✔
1329
    {
1330
      setApiVersionStatus( MerginApiStatus::OK );
11✔
1331
    }
11✔
1332
    else
1333
    {
1334
      setApiVersionStatus( MerginApiStatus::INCOMPATIBLE );
×
1335
    }
1336
  }
11✔
1337
  else
1338
  {
1339
    setApiVersionStatus( MerginApiStatus::NOT_FOUND );
×
1340
  }
1341
}
11✔
1342

1343
bool MerginApi::extractProjectName( const QString &sourceString, QString &projectNamespace, QString &name )
189✔
1344
{
1345
  QStringList parts = sourceString.split( "/" );
189✔
1346
  if ( parts.length() > 1 )
189✔
1347
  {
1348
    projectNamespace = parts.at( parts.length() - 2 );
189✔
1349
    name = parts.last();
189✔
1350
    return true;
189✔
1351
  }
1352
  else
1353
  {
1354
    name = sourceString;
×
1355
    return false;
×
1356
  }
1357
}
189✔
1358

1359
QString MerginApi::extractServerErrorMsg( const QByteArray &data )
49✔
1360
{
1361
  QString serverMsg = "[can't parse server error]";
49✔
1362
  QJsonDocument doc = QJsonDocument::fromJson( data );
49✔
1363
  if ( doc.isObject() )
49✔
1364
  {
1365
    QJsonObject obj = doc.object();
45✔
1366
    if ( obj.contains( QStringLiteral( "detail" ) ) )
45✔
1367
    {
1368
      QJsonValue vDetail = obj.value( "detail" );
43✔
1369
      if ( vDetail.isString() )
43✔
1370
      {
1371
        serverMsg = vDetail.toString();
43✔
1372
      }
43✔
1373
      else if ( vDetail.isObject() )
×
1374
      {
1375
        serverMsg = QJsonDocument( vDetail.toObject() ).toJson();
×
1376
      }
×
1377
    }
43✔
1378
    else if ( obj.contains( QStringLiteral( "name" ) ) )
2✔
1379
    {
1380
      QJsonValue val = obj.value( "name" );
2✔
1381
      if ( val.isArray() )
2✔
1382
      {
1383
        QJsonArray errors = val.toArray();
1✔
1384
        QStringList messages;
1✔
1385
        for ( auto it = errors.constBegin(); it != errors.constEnd(); ++it )
2✔
1386
        {
1387
          messages << it->toString();
1✔
1388
        }
1✔
1389
        serverMsg = messages.join( " " );
1✔
1390
      }
1✔
1391
    }
2✔
1392
    else
1393
    {
1394
      serverMsg = "[can't parse server error]";
×
1395
    }
1396
  }
45✔
1397
  else
1398
  {
1399
    // take only first 1000 bytes of the message ~ there are situations when data is an unclosed string that would eat the whole log memory
1400
    serverMsg = data.mid( 0, 1000 );
4✔
1401
  }
1402

1403
  return serverMsg;
49✔
1404
}
49✔
1405

1406

1407
LocalProject MerginApi::getLocalProject( const QString &projectFullName )
7✔
1408
{
1409
  return mLocalProjects.projectFromMerginName( projectFullName );
7✔
1410
}
1411

1412
ProjectDiff MerginApi::localProjectChanges( const QString &projectDir )
41✔
1413
{
1414
  MerginProjectMetadata projectMetadata = MerginProjectMetadata::fromCachedJson( projectDir + "/" + sMetadataFile );
41✔
1415
  QList<MerginFile> localFiles = getLocalProjectFiles( projectDir + "/" );
41✔
1416

1417
  MerginConfig config = MerginConfig::fromFile( projectDir + "/" + sMerginConfigFile );
41✔
1418

1419
  return compareProjectFiles( projectMetadata.files, projectMetadata.files, localFiles, projectDir, config.isValid, config );
41✔
1420
}
41✔
1421

1422
QString MerginApi::getTempProjectDir( const QString &projectFullName )
337✔
1423
{
1424
  return mDataDir + "/" + TEMP_FOLDER + projectFullName;
337✔
1425
}
×
1426

1427
QString MerginApi::getFullProjectName( QString projectNamespace, QString projectName ) // TODO: move to inpututils?
43,582✔
1428
{
1429
  return QString( "%1/%2" ).arg( projectNamespace ).arg( projectName );
43,582✔
1430
}
×
1431

1432
MerginApiStatus::VersionStatus MerginApi::apiVersionStatus() const
22✔
1433
{
1434
  return mApiVersionStatus;
22✔
1435
}
1436

1437
void MerginApi::setApiVersionStatus( const MerginApiStatus::VersionStatus &apiVersionStatus )
34✔
1438
{
1439
  if ( mApiVersionStatus != apiVersionStatus )
34✔
1440
  {
1441
    mApiVersionStatus = apiVersionStatus;
32✔
1442
    emit apiVersionStatusChanged();
32✔
1443
  }
32✔
1444
}
34✔
1445

1446
void MerginApi::pingMergin()
23✔
1447
{
1448
  if ( mApiVersionStatus == MerginApiStatus::OK ) return;
23✔
1449

1450
  setApiVersionStatus( MerginApiStatus::PENDING );
23✔
1451

1452
  QNetworkRequest request = getDefaultRequest( false );
23✔
1453
  QUrl url( mApiRoot + QStringLiteral( "/ping" ) );
23✔
1454
  request.setUrl( url );
23✔
1455

1456
  QNetworkReply *reply = mManager.get( request );
23✔
1457
  CoreUtils::log( "ping", QStringLiteral( "Requesting: " ) + url.toString() );
23✔
1458
  connect( reply, &QNetworkReply::finished, this, &MerginApi::pingMerginReplyFinished );
23✔
1459
}
23✔
1460

1461
void MerginApi::migrateProjectToMergin( const QString &projectName, const QString &projectNamespace )
3✔
1462
{
1463
  CoreUtils::log( "migrate project", projectName );
3✔
1464
  if ( projectNamespace.isEmpty() )
3✔
1465
  {
1466
    createProject( mUserAuth->username(), projectName );
3✔
1467
  }
3✔
1468
  else
1469
  {
1470
    createProject( projectNamespace, projectName );
×
1471
  }
1472
}
3✔
1473

1474
void MerginApi::detachProjectFromMergin( const QString &projectNamespace, const QString &projectName, bool informUser )
1✔
1475
{
1476
  // Remove mergin folder
1477
  QString projectFullName = getFullProjectName( projectNamespace, projectName );
1✔
1478
  LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName );
1✔
1479

1480
  if ( projectInfo.isValid() )
1✔
1481
  {
1482
    CoreUtils::removeDir( projectInfo.projectDir + "/.mergin" );
1✔
1483
  }
1✔
1484

1485
  // Update localProject
1486
  mLocalProjects.updateNamespace( projectInfo.projectDir, "" );
1✔
1487
  mLocalProjects.updateLocalVersion( projectInfo.projectDir, -1 );
1✔
1488

1489
  if ( informUser )
1✔
1490
    emit notify( tr( "Project detached from Mergin" ) );
1✔
1491

1492
  emit projectDetached( projectFullName );
1✔
1493
}
1✔
1494

1495
QString MerginApi::apiRoot() const
114✔
1496
{
1497
  return mApiRoot;
114✔
1498
}
1499

1500
void MerginApi::setApiRoot( const QString &apiRoot )
4✔
1501
{
1502
  QString newApiRoot;
4✔
1503
  if ( apiRoot.isEmpty() )
4✔
1504
  {
1505
    newApiRoot = defaultApiRoot();
×
1506
  }
×
1507
  else
1508
  {
1509
    newApiRoot = apiRoot;
4✔
1510
  }
1511

1512
  if ( newApiRoot != mApiRoot )
4✔
1513
  {
1514
    mApiRoot = newApiRoot;
2✔
1515

1516
    QSettings settings;
2✔
1517
    settings.setValue( QStringLiteral( "Input/apiRoot" ), mApiRoot );
2✔
1518

1519
    emit apiRootChanged();
2✔
1520
  }
2✔
1521
}
4✔
1522

1523
QString MerginApi::merginUserName() const
27✔
1524
{
1525
  return userAuth()->username();
27✔
1526
}
1527

1528
QList<MerginFile> MerginApi::getLocalProjectFiles( const QString &projectPath )
281✔
1529
{
1530
  QList<MerginFile> merginFiles;
281✔
1531
  QSet<QString> localFiles = listFiles( projectPath );
281✔
1532
  for ( QString p : localFiles )
1,274✔
1533
  {
1534

1535
    MerginFile file;
993✔
1536
    QByteArray localChecksumBytes = getChecksum( projectPath + p );
993✔
1537
    QString localChecksum = QString::fromLatin1( localChecksumBytes.data(), localChecksumBytes.size() );
993✔
1538
    file.checksum = localChecksum;
993✔
1539
    file.path = p;
993✔
1540
    QFileInfo info( projectPath + p );
993✔
1541
    file.size = info.size();
993✔
1542
    file.mtime = info.lastModified();
993✔
1543
    merginFiles.append( file );
993✔
1544
  }
993✔
1545
  return merginFiles;
281✔
1546
}
281✔
1547

1548
void MerginApi::listProjectsReplyFinished( QString requestId )
22✔
1549
{
1550
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
22✔
1551
  Q_ASSERT( r );
22✔
1552

1553
  int projectCount = -1;
22✔
1554
  int requestedPage = 1;
22✔
1555
  MerginProjectsList projectList;
22✔
1556

1557
  if ( r->error() == QNetworkReply::NoError )
22✔
1558
  {
1559
    QUrlQuery query( r->request().url().query() );
22✔
1560
    requestedPage = query.queryItemValue( "page" ).toInt();
22✔
1561

1562
    QByteArray data = r->readAll();
22✔
1563
    QJsonDocument doc = QJsonDocument::fromJson( data );
22✔
1564

1565
    if ( doc.isObject() )
22✔
1566
    {
1567
      projectCount = doc.object().value( "count" ).toInt();
22✔
1568
      projectList = parseProjectsFromJson( doc );
22✔
1569
    }
22✔
1570

1571
    CoreUtils::log( "list projects", QStringLiteral( "Success - got %1 projects" ).arg( projectList.count() ) );
22✔
1572
  }
22✔
1573
  else
1574
  {
1575
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
1576
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listProjects" ), r->errorString(), serverMsg );
×
1577
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: listProjects" ) );
×
1578
    CoreUtils::log( "list projects", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
1579

1580
    emit listProjectsFailed();
×
1581
  }
×
1582

1583
  r->deleteLater();
22✔
1584

1585
  emit listProjectsFinished( projectList, projectCount, requestedPage, requestId );
22✔
1586
}
22✔
1587

1588
void MerginApi::listProjectsByNameReplyFinished( QString requestId )
7✔
1589
{
1590
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
7✔
1591
  Q_ASSERT( r );
7✔
1592

1593
  MerginProjectsList projectList;
7✔
1594

1595
  if ( r->error() == QNetworkReply::NoError )
7✔
1596
  {
1597
    QByteArray data = r->readAll();
7✔
1598
    QJsonDocument json = QJsonDocument::fromJson( data );
7✔
1599
    projectList = parseProjectsFromJson( json );
7✔
1600
    CoreUtils::log( "list projects by name", QStringLiteral( "Success - got %1 projects" ).arg( projectList.count() ) );
7✔
1601
  }
7✔
1602
  else
1603
  {
1604
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
1605
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listProjectsByName" ), r->errorString(), serverMsg );
×
1606
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: listProjectsByName" ) );
×
1607
    CoreUtils::log( "list projects by name", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
1608

1609
    emit listProjectsFailed();
×
1610
  }
×
1611

1612
  r->deleteLater();
7✔
1613

1614
  emit listProjectsByNameFinished( projectList, requestId );
7✔
1615
}
7✔
1616

1617

1618
void MerginApi::finalizeProjectPullCopy( const QString &projectFullName, const QString &projectDir, const QString &tempDir, const QString &filePath, const QList<DownloadQueueItem> &items )
202✔
1619
{
1620
  CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Copying new content of " ) + filePath );
202✔
1621

1622
  QString dest = projectDir + "/" + filePath;
202✔
1623
  createPathIfNotExists( dest );
202✔
1624

1625
  QFile f( dest );
202✔
1626
  if ( !f.open( QIODevice::WriteOnly ) )
202✔
1627
  {
1628
    CoreUtils::log( "pull " + projectFullName, "Failed to open file for writing " + dest );
×
1629
    return;
×
1630
  }
1631

1632
  // assemble file from tmp files
1633
  for ( const auto &item : items )
343✔
1634
  {
1635
    QFile fTmp( tempDir + "/" + item.tempFileName );
141✔
1636
    if ( !fTmp.open( QIODevice::ReadOnly ) )
141✔
1637
    {
1638
      CoreUtils::log( "pull " + projectFullName, "Failed to open temp file for reading " + item.tempFileName );
×
1639
      return;
×
1640
    }
1641
    f.write( fTmp.readAll() );
141✔
1642
  }
141✔
1643

1644
  f.close();
202✔
1645

1646
  // if diffable, copy to .mergin dir so we have a basefile
1647
  if ( MerginApi::isFileDiffable( filePath ) )
202✔
1648
  {
1649
    QString basefile = projectDir + "/.mergin/" + filePath;
15✔
1650
    createPathIfNotExists( basefile );
15✔
1651

1652
    if ( !QFile::remove( basefile ) )
15✔
1653
    {
1654
      CoreUtils::log( "pull " + projectFullName, "failed to remove old basefile for: " + filePath );
15✔
1655
    }
15✔
1656
    if ( !QFile::copy( dest, basefile ) )
15✔
1657
    {
1658
      CoreUtils::log( "pull " + projectFullName, "failed to copy new basefile for: " + filePath );
×
1659
    }
×
1660
  }
15✔
1661
}
202✔
1662

1663

1664
bool MerginApi::finalizeProjectPullApplyDiff( const QString &projectFullName, const QString &projectDir, const QString &tempDir, const QString &filePath, const QList<DownloadQueueItem> &items )
9✔
1665
{
1666
  CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Applying diff to " ) + filePath );
9✔
1667

1668
  // update diffable files that have been modified on the server
1669
  // - if they were not modified locally, the server changes will be simply applied
1670
  // - if they were modified locally, local changes will be rebased on top of server changes
1671

1672
  QString src = tempDir + "/" + CoreUtils::uuidWithoutBraces( QUuid::createUuid() );
9✔
1673
  QString dest = projectDir + "/" + filePath;
9✔
1674
  QString basefile = projectDir + "/.mergin/" + filePath;
9✔
1675

1676
  LocalProject info = mLocalProjects.projectFromMerginName( projectFullName );
9✔
1677

1678
  // add conflict files to project dir so they can be synced
1679
  QString conflictfile = CoreUtils::findUniquePath( CoreUtils::generateEditConflictFileName( dest, mUserAuth->username(), info.localVersion ) );
9✔
1680

1681
  createPathIfNotExists( src );
9✔
1682
  createPathIfNotExists( dest );
9✔
1683
  createPathIfNotExists( basefile );
9✔
1684

1685
  QStringList diffFiles;
9✔
1686
  for ( const auto &item : items )
19✔
1687
    diffFiles << tempDir + "/" + item.tempFileName;
10✔
1688

1689
  //
1690
  // let's first assemble server's file from our basefile + diffs
1691
  //
1692

1693
  if ( !QFile::copy( basefile, src ) )
9✔
1694
  {
1695
    CoreUtils::log( "pull " + projectFullName, "assemble server file fail: copying failed " + basefile + " to " + src );
×
1696

1697
    // TODO: this is a critical failure - we should abort pull
1698
  }
×
1699

1700
  if ( !GeodiffUtils::applyDiffs( src, diffFiles ) )
9✔
1701
  {
1702
    CoreUtils::log( "pull " + projectFullName, "server file assembly failed: " + filePath );
×
1703

1704
    // TODO: this is a critical failure - we should abort pull
1705
    // TODO: we could try to delete the basefile and re-download it from scratch on next sync
1706
  }
×
1707
  else
1708
  {
1709
    CoreUtils::log( "pull " + projectFullName, "server file assembly successful: " + filePath );
9✔
1710
  }
1711

1712
  //
1713
  // now we are ready for the update of our local file
1714
  //
1715
  bool hasConflicts = false;
9✔
1716

1717
  int res = GEODIFF_rebase( basefile.toUtf8().constData(),
18✔
1718
                            src.toUtf8().constData(),
9✔
1719
                            dest.toUtf8().constData(),
9✔
1720
                            conflictfile.toUtf8().constData()
9✔
1721
                          );
1722
  if ( res == GEODIFF_SUCCESS )
9✔
1723
  {
1724
    CoreUtils::log( "pull " + projectFullName, "geodiff rebase successful: " + filePath );
8✔
1725
  }
8✔
1726
  else
1727
  {
1728
    CoreUtils::log( "pull " + projectFullName, "geodiff rebase failed! " + filePath );
1✔
1729

1730
    // not good... something went wrong in rebase - we need to save the local changes
1731
    // let's put them into a conflict file and use the server version
1732
    hasConflicts = true;
1✔
1733
    LocalProject info = mLocalProjects.projectFromMerginName( projectFullName );
1✔
1734
    QString newDest = CoreUtils::findUniquePath( CoreUtils::generateConflictedCopyFileName( dest, mUserAuth->username(), info.localVersion ) );
1✔
1735
    if ( !QFile::rename( dest, newDest ) )
1✔
1736
    {
1737
      CoreUtils::log( "pull " + projectFullName, "failed rename of conflicting file after failed geodiff rebase: " + filePath );
×
1738
    }
×
1739
    if ( !QFile::copy( src, dest ) )
1✔
1740
    {
1741
      CoreUtils::log( "pull " + projectFullName, "failed to update local conflicting file after failed geodiff rebase: " + filePath );
×
1742
    }
×
1743
  }
1✔
1744

1745
  //
1746
  // finally update our basefile
1747
  //
1748

1749
  if ( !QFile::remove( basefile ) )
9✔
1750
  {
1751
    CoreUtils::log( "pull " + projectFullName, "failed removal of old basefile: " + filePath );
×
1752

1753
    // TODO: this is a critical failure - we should abort pull
1754
  }
×
1755
  if ( !QFile::rename( src, basefile ) )
9✔
1756
  {
1757
    CoreUtils::log( "pull " + projectFullName, "failed rename of basefile using new server content: " + filePath );
×
1758

1759
    // TODO: this is a critical failure - we should abort pull
1760
  }
×
1761
  return hasConflicts;
9✔
1762
}
9✔
1763

1764
void MerginApi::finalizeProjectPull( const QString &projectFullName )
123✔
1765
{
1766
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
123✔
1767
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
123✔
1768

1769
  QString projectDir = transaction.projectDir;
123✔
1770
  QString tempProjectDir = getTempProjectDir( projectFullName );
123✔
1771

1772
  CoreUtils::log( "pull " + projectFullName, "Running update tasks" );
123✔
1773

1774
  for ( const PullTask &finalizationItem : transaction.pullTasks )
337✔
1775
  {
1776
    switch ( finalizationItem.method )
214✔
1777
    {
1778
      case PullTask::Copy:
1779
      {
1780
        finalizeProjectPullCopy( projectFullName, projectDir, tempProjectDir, finalizationItem.filePath, finalizationItem.data );
199✔
1781
        break;
199✔
1782
      }
1783

1784
      case PullTask::CopyConflict:
1785
      {
1786
        // move local file to conflict file
1787
        QString origPath = projectDir + "/" + finalizationItem.filePath;
3✔
1788
        LocalProject info = mLocalProjects.projectFromMerginName( projectFullName );
3✔
1789
        QString newPath = CoreUtils::findUniquePath( CoreUtils::generateConflictedCopyFileName( origPath, mUserAuth->username(), info.localVersion ) );
3✔
1790
        if ( !QFile::rename( origPath, newPath ) )
3✔
1791
        {
1792
          CoreUtils::log( "pull " + projectFullName, "failed rename of conflicting file: " + finalizationItem.filePath );
×
1793
        }
×
1794
        else
1795
        {
1796
          CoreUtils::log( "pull " + projectFullName, "Local file renamed due to conflict with server: " + finalizationItem.filePath );
3✔
1797
        }
1798
        finalizeProjectPullCopy( projectFullName, projectDir, tempProjectDir, finalizationItem.filePath, finalizationItem.data );
3✔
1799
        break;
1800
      }
3✔
1801

1802
      case PullTask::ApplyDiff:
1803
      {
1804
        // applying diff can result in conflicted copy too, in this case
1805
        // we need to update gpkgSchemaChanged flag.
1806
        bool res = finalizeProjectPullApplyDiff( projectFullName, projectDir, tempProjectDir, finalizationItem.filePath, finalizationItem.data );
9✔
1807
        transaction.gpkgSchemaChanged = res;
9✔
1808
        break;
9✔
1809
      }
1810

1811
      case PullTask::Delete:
1812
      {
1813
        CoreUtils::log( "pull " + projectFullName, "Removing local file: " + finalizationItem.filePath );
3✔
1814
        QFile file( projectDir + "/" + finalizationItem.filePath );
3✔
1815
        file.remove();
3✔
1816
        break;
1817
      }
3✔
1818
    }
1819

1820
    // remove tmp files associated with this item
1821
    for ( const auto &downloadItem : finalizationItem.data )
365✔
1822
    {
1823
      if ( !QFile::remove( tempProjectDir + "/" + downloadItem.tempFileName ) )
151✔
1824
        CoreUtils::log( "pull " + projectFullName, "Failed to remove temporary file " + downloadItem.tempFileName );
×
1825
    }
1826
  }
1827

1828
  // check there are no files left
1829
  int tmpFilesLeft = QDir( tempProjectDir ).entryList( QDir::NoDotAndDotDot ).count();
123✔
1830
  if ( tmpFilesLeft )
123✔
1831
  {
1832
    CoreUtils::log( "pull " + projectFullName, "Some temporary files were left - this should not happen..." );
×
1833
  }
×
1834

1835
  QDir( tempProjectDir ).removeRecursively();
123✔
1836

1837
  // add the local project if not there yet
1838
  if ( !mLocalProjects.projectFromMerginName( projectFullName ).isValid() )
123✔
1839
  {
1840
    QString projectNamespace, projectName;
61✔
1841
    extractProjectName( projectFullName, projectNamespace, projectName );
61✔
1842

1843
    // remove download in progress file
1844
    if ( !QFile::remove( CoreUtils::downloadInProgressFilePath( transaction.projectDir ) ) )
61✔
1845
      CoreUtils::log( QStringLiteral( "sync %1" ).arg( projectFullName ), QStringLiteral( "Failed to remove download in progress file for project name %1" ).arg( projectName ) );
×
1846

1847
    mLocalProjects.addMerginProject( projectDir, projectNamespace, projectName );
61✔
1848
  }
61✔
1849

1850
  finishProjectSync( projectFullName, true );
123✔
1851
}
123✔
1852

1853

1854
void MerginApi::pushStartReplyFinished()
115✔
1855
{
1856
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
115✔
1857
  Q_ASSERT( r );
115✔
1858

1859
  QString projectFullName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ) ).toString();
115✔
1860

1861
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
115✔
1862
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
115✔
1863
  Q_ASSERT( r == transaction.replyPushStart );
115✔
1864

1865
  if ( r->error() == QNetworkReply::NoError )
115✔
1866
  {
1867
    QByteArray data = r->readAll();
115✔
1868

1869
    transaction.replyPushStart->deleteLater();
115✔
1870
    transaction.replyPushStart = nullptr;
115✔
1871

1872
    QList<MerginFile> files = transaction.pushQueue;
115✔
1873
    if ( !files.isEmpty() )
115✔
1874
    {
1875
      QString transactionUUID;
111✔
1876
      QJsonDocument doc = QJsonDocument::fromJson( data );
111✔
1877
      if ( doc.isObject() )
111✔
1878
      {
1879
        QJsonObject docObj = doc.object();
111✔
1880
        transactionUUID = docObj.value( QStringLiteral( "transaction" ) ).toString();
111✔
1881
        transaction.transactionUUID = transactionUUID;
111✔
1882
      }
111✔
1883

1884
      if ( transaction.transactionUUID.isEmpty() )
111✔
1885
      {
1886
        CoreUtils::log( "push " + projectFullName, QStringLiteral( "Fail! Could not acquire transaction ID" ) );
×
1887
        finishProjectSync( projectFullName, false );
×
1888
      }
×
1889

1890
      CoreUtils::log( "push " + projectFullName, QStringLiteral( "Push request accepted. Transaction ID: " ) + transactionUUID );
111✔
1891

1892
      MerginFile file = files.first();
111✔
1893
      pushFile( projectFullName, transactionUUID, file );
111✔
1894
      emit pushFilesStarted();
111✔
1895
    }
111✔
1896
    else  // pushing only files to be removed
1897
    {
1898
      // we are done here - no upload of chunks, no request to "finish"
1899
      // because server immediatelly creates a new version without starting a transaction to upload chunks
1900

1901
      CoreUtils::log( "push " + projectFullName, QStringLiteral( "Push request accepted and no files to upload" ) );
4✔
1902

1903
      transaction.projectMetadata = data;
4✔
1904
      transaction.version = MerginProjectMetadata::fromJson( data ).version;
4✔
1905

1906
      finishProjectSync( projectFullName, true );
4✔
1907
    }
1908
  }
115✔
1909
  else
1910
  {
1911
    QVariant statusCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute );
×
1912
    int status = statusCode.toInt();
×
1913
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
1914
    QString errorMsg = r->errorString();
×
1915
    bool showLimitReachedDialog = status == 400 && serverMsg.contains( QStringLiteral( "You have reached a data limit" ) );
×
1916

1917
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) );
×
1918

1919
    transaction.replyPushStart->deleteLater();
×
1920
    transaction.replyPushStart = nullptr;
×
1921

1922
    if ( showLimitReachedDialog )
×
1923
    {
1924
      const QList<MerginFile> files = transaction.pushQueue;
×
1925
      qreal uploadSize = 0;
×
1926
      for ( const MerginFile &f : files )
×
1927
      {
1928
        uploadSize += f.size;
×
1929
      }
1930
      emit storageLimitReached( uploadSize );
×
1931

1932
      // remove project if it was first time sync - migration
1933
      if ( transaction.isInitialPush )
×
1934
      {
1935
        QString projectNamespace, projectName;
×
1936
        extractProjectName( projectFullName, projectNamespace, projectName );
×
1937

1938
        detachProjectFromMergin( projectNamespace, projectName, false );
×
1939
        deleteProject( projectNamespace, projectName, false );
×
1940
      }
×
1941
    }
×
1942
    else
1943
    {
1944
      int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
×
1945
      emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: pushStartReply" ), httpCode, projectFullName );
×
1946
    }
1947

1948
    finishProjectSync( projectFullName, false );
×
1949
  }
×
1950
}
115✔
1951

1952
void MerginApi::pushFileReplyFinished()
152✔
1953
{
1954
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
152✔
1955
  Q_ASSERT( r );
152✔
1956

1957
  QString projectFullName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ) ).toString();
152✔
1958

1959
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
152✔
1960
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
152✔
1961
  Q_ASSERT( r == transaction.replyPushFile );
152✔
1962

1963
  QStringList params = ( r->url().toString().split( "/" ) );
152✔
1964
  QString transactionUUID = params.at( params.length() - 2 );
152✔
1965
  QString chunkID = params.at( params.length() - 1 );
152✔
1966
  Q_ASSERT( transactionUUID == transaction.transactionUUID );
152✔
1967

1968
  if ( r->error() == QNetworkReply::NoError )
152✔
1969
  {
1970
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Uploaded successfully: " ) + chunkID );
151✔
1971

1972
    transaction.replyPushFile->deleteLater();
151✔
1973
    transaction.replyPushFile = nullptr;
151✔
1974

1975
    MerginFile currentFile = transaction.pushQueue.first();
151✔
1976
    int chunkNo = currentFile.chunks.indexOf( chunkID );
151✔
1977
    if ( chunkNo < currentFile.chunks.size() - 1 )
151✔
1978
    {
1979
      pushFile( projectFullName, transactionUUID, currentFile, chunkNo + 1 );
2✔
1980
    }
2✔
1981
    else
1982
    {
1983
      transaction.transferedSize += currentFile.size;
149✔
1984

1985
      emit syncProjectStatusChanged( projectFullName, transaction.transferedSize / transaction.totalSize );
149✔
1986
      transaction.pushQueue.removeFirst();
149✔
1987

1988
      if ( !transaction.pushQueue.isEmpty() )
149✔
1989
      {
1990
        MerginFile nextFile = transaction.pushQueue.first();
39✔
1991
        pushFile( projectFullName, transactionUUID, nextFile );
39✔
1992
      }
39✔
1993
      else
1994
      {
1995
        pushFinish( projectFullName, transactionUUID );
110✔
1996
      }
1997
    }
1998
  }
151✔
1999
  else
2000
  {
2001
    QString serverMsg = extractServerErrorMsg( r->readAll() );
1✔
2002
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) );
1✔
2003

2004
    int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
1✔
2005
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: pushFile" ), httpCode, projectFullName );
1✔
2006

2007
    transaction.replyPushFile->deleteLater();
1✔
2008
    transaction.replyPushFile = nullptr;
1✔
2009

2010
    finishProjectSync( projectFullName, false );
1✔
2011
  }
1✔
2012
}
152✔
2013

2014
void MerginApi::pullInfoReplyFinished()
100✔
2015
{
2016
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
100✔
2017
  Q_ASSERT( r );
100✔
2018

2019
  QString projectFullName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ) ).toString();
100✔
2020

2021
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
100✔
2022
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
100✔
2023
  Q_ASSERT( r == transaction.replyPullProjectInfo );
100✔
2024

2025
  if ( r->error() == QNetworkReply::NoError )
100✔
2026
  {
2027
    QByteArray data = r->readAll();
99✔
2028
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Downloaded project info." ) );
99✔
2029

2030
    transaction.replyPullProjectInfo->deleteLater();
99✔
2031
    transaction.replyPullProjectInfo = nullptr;
99✔
2032

2033
    prepareProjectPull( projectFullName, data );
99✔
2034
  }
99✔
2035
  else
2036
  {
2037
    QString serverMsg = extractServerErrorMsg( r->readAll() );
1✔
2038
    QString message = QStringLiteral( "Network API error: %1(): %2" ).arg( QStringLiteral( "projectInfo" ), r->errorString() );
1✔
2039
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) );
1✔
2040

2041
    int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
1✔
2042
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: pullInfo" ), httpCode, projectFullName );
1✔
2043

2044
    transaction.replyPullProjectInfo->deleteLater();
1✔
2045
    transaction.replyPullProjectInfo = nullptr;
1✔
2046

2047
    finishProjectSync( projectFullName, false );
1✔
2048
  }
1✔
2049
}
100✔
2050

2051
void MerginApi::prepareProjectPull( const QString &projectFullName, const QByteArray &data )
125✔
2052
{
2053
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
125✔
2054
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
125✔
2055

2056
  MerginProjectMetadata serverProject = MerginProjectMetadata::fromJson( data );
125✔
2057

2058
  transaction.projectMetadata = data;
125✔
2059
  transaction.version = serverProject.version;
125✔
2060

2061
  LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName );
125✔
2062
  if ( projectInfo.isValid() )
125✔
2063
  {
2064
    transaction.projectDir = projectInfo.projectDir;
63✔
2065

2066
    // do not continue if we are already on the latest version
2067
    if ( projectInfo.localVersion != -1 && projectInfo.localVersion == serverProject.version )
63✔
2068
    {
2069
      emit projectAlreadyOnLatestVersion( projectFullName );
1✔
2070
      CoreUtils::log( QStringLiteral( "Pull %1" ).arg( projectFullName ), QStringLiteral( "Project is already on the latest version: %1" ).arg( serverProject.version ) );
1✔
2071

2072
      return finishProjectSync( projectFullName, false );
1✔
2073
    }
2074
  }
62✔
2075
  else
2076
  {
2077
    QString projectNamespace;
62✔
2078
    QString projectName;
62✔
2079
    extractProjectName( projectFullName, projectNamespace, projectName );
62✔
2080

2081
    // remove any leftover temp files that could be created from previous unsuccessful download
2082
    removeProjectsTempFolder( projectNamespace, projectName );
62✔
2083

2084
    // project has not been downloaded yet - we need to create a directory for it
2085
    transaction.projectDir = CoreUtils::createUniqueProjectDirectory( mDataDir, projectName );
62✔
2086
    transaction.firstTimeDownload = true;
62✔
2087

2088
    // create file indicating first time download in progress
2089
    QString downloadInProgressFilePath = CoreUtils::downloadInProgressFilePath( transaction.projectDir );
62✔
2090
    createPathIfNotExists( downloadInProgressFilePath );
62✔
2091
    if ( !CoreUtils::createEmptyFile( downloadInProgressFilePath ) )
62✔
2092
      CoreUtils::log( QStringLiteral( "pull %1" ).arg( projectFullName ), "Unable to create temporary download in progress file" );
×
2093

2094
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "First time download - new directory: " ) + transaction.projectDir );
62✔
2095
  }
62✔
2096

2097
  Q_ASSERT( !transaction.projectDir.isEmpty() );  // that would mean we do not have entry -> fail getting local files
124✔
2098

2099
  if ( transaction.configAllowed )
124✔
2100
  {
2101
    prepareDownloadConfig( projectFullName );
112✔
2102
  }
112✔
2103
  else
2104
  {
2105
    startProjectPull( projectFullName );
12✔
2106
  }
2107
}
125✔
2108

2109
void MerginApi::startProjectPull( const QString &projectFullName )
124✔
2110
{
2111
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
124✔
2112
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
124✔
2113

2114
  QList<MerginFile> localFiles = getLocalProjectFiles( transaction.projectDir + "/" );
124✔
2115
  MerginProjectMetadata serverProject = MerginProjectMetadata::fromJson( transaction.projectMetadata );
124✔
2116
  MerginProjectMetadata oldServerProject = MerginProjectMetadata::fromCachedJson( transaction.projectDir + "/" + sMetadataFile );
124✔
2117
  MerginConfig oldTransactionConfig = MerginConfig::fromFile( transaction.projectDir + "/" + sMerginConfigFile );
124✔
2118

2119
  CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Updating from version %1 to version %2" )
248✔
2120
                  .arg( oldServerProject.version ).arg( serverProject.version ) );
124✔
2121

2122
  transaction.diff = compareProjectFiles(
124✔
2123
                       oldServerProject.files,
124✔
2124
                       serverProject.files,
124✔
2125
                       localFiles,
2126
                       transaction.projectDir,
124✔
2127
                       transaction.configAllowed,
124✔
2128
                       transaction.config,
124✔
2129
                       oldTransactionConfig );
2130

2131
  CoreUtils::log( "pull " + projectFullName, transaction.diff.dump() );
124✔
2132

2133
  for ( QString filePath : transaction.diff.remoteAdded )
317✔
2134
  {
2135
    MerginFile file = serverProject.fileInfo( filePath );
193✔
2136
    QList<DownloadQueueItem> items = itemsForFileChunks( file, transaction.version );
193✔
2137
    transaction.pullTasks << PullTask( PullTask::Copy, filePath, items );
193✔
2138
    transaction.gpkgSchemaChanged = true;
193✔
2139
  }
193✔
2140

2141
  for ( QString filePath : transaction.diff.remoteUpdated )
138✔
2142
  {
2143
    MerginFile file = serverProject.fileInfo( filePath );
14✔
2144

2145
    // for diffable files - download and apply to the basefile (without rebase)
2146
    if ( isFileDiffable( filePath ) && file.pullCanUseDiff )
14✔
2147
    {
2148
      QList<DownloadQueueItem> items = itemsForFileDiffs( file );
6✔
2149
      transaction.pullTasks << PullTask( PullTask::ApplyDiff, filePath, items );
6✔
2150
    }
6✔
2151
    else
2152
    {
2153
      QList<DownloadQueueItem> items = itemsForFileChunks( file, transaction.version );
8✔
2154
      transaction.pullTasks << PullTask( PullTask::Copy, filePath, items );
8✔
2155
      transaction.gpkgSchemaChanged = true;
8✔
2156
    }
8✔
2157
  }
14✔
2158

2159
  // also download files which were changed both on the server and locally (the local version will be renamed as conflicting copy)
2160
  for ( QString filePath : transaction.diff.conflictRemoteUpdatedLocalUpdated )
129✔
2161
  {
2162
    MerginFile file = serverProject.fileInfo( filePath );
5✔
2163

2164
    // for diffable files - download and apply to the basefile (will also do rebase)
2165
    if ( isFileDiffable( filePath ) && file.pullCanUseDiff )
5✔
2166
    {
2167
      QList<DownloadQueueItem> items = itemsForFileDiffs( file );
3✔
2168
      transaction.pullTasks << PullTask( PullTask::ApplyDiff, filePath, items );
3✔
2169
    }
3✔
2170
    else
2171
    {
2172
      QList<DownloadQueueItem> items = itemsForFileChunks( file, transaction.version );
2✔
2173
      transaction.pullTasks << PullTask( PullTask::CopyConflict, filePath, items );
2✔
2174
      transaction.gpkgSchemaChanged = true;
2✔
2175
    }
2✔
2176
  }
5✔
2177

2178
  // also download files which were added both on the server and locally (the local version will be renamed as conflicting copy)
2179
  for ( QString filePath : transaction.diff.conflictRemoteAddedLocalAdded )
125✔
2180
  {
2181
    MerginFile file = serverProject.fileInfo( filePath );
1✔
2182
    QList<DownloadQueueItem> items = itemsForFileChunks( file, transaction.version );
1✔
2183
    transaction.pullTasks << PullTask( PullTask::CopyConflict, filePath, items );
1✔
2184
    transaction.gpkgSchemaChanged = true;
1✔
2185
  }
1✔
2186

2187
  // schedule removed files to be deleted
2188
  for ( QString filePath : transaction.diff.remoteDeleted )
127✔
2189
  {
2190
    transaction.pullTasks << PullTask( PullTask::Delete, filePath, QList<DownloadQueueItem>() );
3✔
2191
  }
3✔
2192

2193
  // prepare the download queue
2194
  for ( const PullTask &item : transaction.pullTasks )
340✔
2195
  {
2196
    transaction.downloadQueue << item.data;
216✔
2197
  }
2198

2199
  qint64 totalSize = 0;
124✔
2200
  for ( const DownloadQueueItem &item : transaction.downloadQueue )
277✔
2201
  {
2202
    totalSize += item.size;
153✔
2203
  }
2204
  transaction.totalSize = totalSize;
124✔
2205

2206
  CoreUtils::log( "pull " + projectFullName, QStringLiteral( "%1 update tasks, %2 items to download (total size %3 bytes)" )
248✔
2207
                  .arg( transaction.pullTasks.count() )
124✔
2208
                  .arg( transaction.downloadQueue.count() )
124✔
2209
                  .arg( transaction.totalSize ) );
124✔
2210

2211
  emit pullFilesStarted();
124✔
2212
  downloadNextItem( projectFullName );
124✔
2213
}
124✔
2214

2215
void MerginApi::prepareDownloadConfig( const QString &projectFullName, bool downloaded )
151✔
2216
{
2217
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
151✔
2218
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
151✔
2219

2220
  MerginProjectMetadata newServerVersion = MerginProjectMetadata::fromJson( transaction.projectMetadata );
151✔
2221

2222
  const auto res = std::find_if( newServerVersion.files.begin(), newServerVersion.files.end(), []( const MerginFile & file )
676✔
2223
  {
2224
    return file.path == sMerginConfigFile;
525✔
2225
  } );
2226
  bool serverContainsConfig = res != newServerVersion.files.end();
151✔
2227

2228
  if ( serverContainsConfig )
151✔
2229
  {
2230
    if ( !downloaded )
78✔
2231
    {
2232
      // we should have server config but we do not have it yet
2233
      return requestServerConfig( projectFullName );
39✔
2234
    }
2235
  }
39✔
2236

2237
  MerginProjectMetadata oldServerVersion = MerginProjectMetadata::fromCachedJson( transaction.projectDir + "/" + sMetadataFile );
112✔
2238

2239
  const auto resOld = std::find_if( oldServerVersion.files.begin(), oldServerVersion.files.end(), []( const MerginFile & file )
316✔
2240
  {
2241
    return file.path == sMerginConfigFile;
204✔
2242
  } );
2243

2244
  bool previousVersionContainedConfig = ( resOld != oldServerVersion.files.end() ) && !transaction.firstTimeDownload;
112✔
2245

2246
  if ( !transaction.config.isValid )
112✔
2247
  {
2248
    // if transaction is not valid (or missing), consider it as deleted
2249
    transaction.config.downloadMissingFiles = true;
74✔
2250
    CoreUtils::log( "MerginConfig", "No config detected" );
74✔
2251
  }
74✔
2252
  else if ( serverContainsConfig && previousVersionContainedConfig )
38✔
2253
  {
2254
    // config was there, check if there are changes
2255
    QString newChk = newServerVersion.fileInfo( sMerginConfigFile ).checksum;
29✔
2256
    QString oldChk = oldServerVersion.fileInfo( sMerginConfigFile ).checksum;
29✔
2257

2258
    if ( newChk == oldChk )
29✔
2259
    {
2260
      // config files are the same
2261
    }
21✔
2262
    else
2263
    {
2264
      // config was changed, but what changed?
2265
      MerginConfig oldConfig = MerginConfig::fromFile( transaction.projectDir + "/" + MerginApi::sMerginConfigFile );
8✔
2266

2267
      if ( oldConfig.selectiveSyncEnabled != transaction.config.selectiveSyncEnabled )
8✔
2268
      {
2269
        // selective sync was enabled/disabled
2270
        if ( transaction.config.selectiveSyncEnabled )
4✔
2271
        {
2272
          CoreUtils::log( "MerginConfig", "Selective sync has been enabled" );
2✔
2273
        }
2✔
2274
        else
2275
        {
2276
          CoreUtils::log( "MerginConfig", "Selective sync has been disabled, downloading missing files." );
2✔
2277
          transaction.config.downloadMissingFiles = true;
2✔
2278
        }
2279
      }
4✔
2280
      else if ( oldConfig.selectiveSyncDir != transaction.config.selectiveSyncDir )
4✔
2281
      {
2282
        CoreUtils::log( "MerginConfig", "Selective sync directory has changed, downloading missing files." );
4✔
2283
        transaction.config.downloadMissingFiles = true;
4✔
2284
      }
4✔
2285
      else
2286
      {
2287
        CoreUtils::log( "MerginConfig", "Unknown change in config file, continuing with latest version." );
×
2288
      }
2289
    }
8✔
2290
  }
29✔
2291
  else if ( serverContainsConfig )
9✔
2292
  {
2293
    CoreUtils::log( "MerginConfig", "Detected new config file." );
9✔
2294
  }
9✔
2295
  else if ( previousVersionContainedConfig ) // and current does not
×
2296
  {
2297
    CoreUtils::log( "MerginConfig", "Config file was removed, downloading missing files." );
×
2298
    transaction.config.downloadMissingFiles = true;
×
2299
  }
×
2300
  else // no config in last versions
2301
  {
2302
    // pull like without config
2303
    transaction.configAllowed = false;
×
2304
    transaction.config.isValid = false;
×
2305

2306
    // if it would be possible to add mergin-config locally, it needs to be checked here
2307
  }
2308

2309
  startProjectPull( projectFullName );
112✔
2310
}
151✔
2311

2312
void MerginApi::requestServerConfig( const QString &projectFullName )
39✔
2313
{
2314
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
39✔
2315
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
39✔
2316

2317
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/raw/" ) + projectFullName );
39✔
2318
  QUrlQuery query;
39✔
2319

2320
  query.addQueryItem( "file", sMerginConfigFile.toUtf8().toPercentEncoding() );
39✔
2321
  query.addQueryItem( "version", QStringLiteral( "v%1" ).arg( transaction.version ) );
39✔
2322
  url.setQuery( query );
39✔
2323

2324
  QNetworkRequest request = getDefaultRequest();
39✔
2325
  request.setUrl( url );
39✔
2326
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
39✔
2327

2328
  Q_ASSERT( !transaction.replyPullItem );
39✔
2329
  transaction.replyPullItem = mManager.get( request );
39✔
2330
  connect( transaction.replyPullItem, &QNetworkReply::finished, this, &MerginApi::cacheServerConfig );
39✔
2331

2332
  CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Requesting mergin config: " ) + url.toString() );
39✔
2333
}
39✔
2334

2335
QList<DownloadQueueItem> MerginApi::itemsForFileChunks( const MerginFile &file, int version )
204✔
2336
{
2337
  QList<DownloadQueueItem> lst;
204✔
2338
  int from = 0;
204✔
2339
  while ( from < file.size )
347✔
2340
  {
2341
    int size = qMin( MerginApi::UPLOAD_CHUNK_SIZE, static_cast<int>( file.size ) - from );
143✔
2342
    lst << DownloadQueueItem( file.path, size, version, from, from + size - 1 );
143✔
2343
    from += size;
143✔
2344
  }
2345
  return lst;
204✔
2346
}
204✔
2347

2348
QList<DownloadQueueItem> MerginApi::itemsForFileDiffs( const MerginFile &file )
9✔
2349
{
2350
  QList<DownloadQueueItem> items;
9✔
2351
  // download diffs instead of full download of gpkg file from server
2352
  for ( const auto &d : file.pullDiffFiles )
19✔
2353
  {
2354
    items << DownloadQueueItem( file.path, d.second, d.first, -1, -1, true );
10✔
2355
  }
2356
  return items;
9✔
2357
}
9✔
2358

2359

2360
static MerginFile findFile( const QString &filePath, const QList<MerginFile> &files )
155✔
2361
{
2362
  for ( const MerginFile &merginFile : files )
390✔
2363
  {
2364
    if ( merginFile.path == filePath )
390✔
2365
      return merginFile;
155✔
2366
  }
2367
  CoreUtils::log( QStringLiteral( "MerginFile" ), QStringLiteral( "requested findFile() for non-existant file: %1" ).arg( filePath ) );
×
2368
  return MerginFile();
×
2369
}
155✔
2370

2371

2372
void MerginApi::pushInfoReplyFinished()
143✔
2373
{
2374
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
143✔
2375
  Q_ASSERT( r );
143✔
2376

2377
  QString projectFullName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ) ).toString();
143✔
2378

2379
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
143✔
2380
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
143✔
2381
  Q_ASSERT( r == transaction.replyPushProjectInfo );
143✔
2382

2383
  if ( r->error() == QNetworkReply::NoError )
143✔
2384
  {
2385
    QString url = r->url().toString();
142✔
2386
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Downloaded project info." ) );
142✔
2387
    QByteArray data = r->readAll();
142✔
2388

2389
    transaction.replyPushProjectInfo->deleteLater();
142✔
2390
    transaction.replyPushProjectInfo = nullptr;
142✔
2391

2392
    LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName );
142✔
2393
    transaction.projectDir = projectInfo.projectDir;
142✔
2394
    Q_ASSERT( !transaction.projectDir.isEmpty() );
142✔
2395

2396
    // get the latest server version from our reply (we do not update it in LocalProjectsManager though... I guess we don't need to)
2397
    MerginProjectMetadata serverProject = MerginProjectMetadata::fromJson( data );
142✔
2398

2399
    // now let's figure a key question: are we on the most recent version of the project
2400
    // if we're about to do upload? because if not, we need to do pull first
2401
    if ( projectInfo.isValid() && projectInfo.localVersion != -1 && projectInfo.localVersion < serverProject.version )
142✔
2402
    {
2403
      CoreUtils::log( "push " + projectFullName, QStringLiteral( "Need pull first: local version %1 | server version %2" )
52✔
2404
                      .arg( projectInfo.localVersion ).arg( serverProject.version ) );
26✔
2405
      transaction.pullBeforePush = true;
26✔
2406
      prepareProjectPull( projectFullName, data );
26✔
2407
      return;
26✔
2408
    }
2409

2410
    QList<MerginFile> localFiles = getLocalProjectFiles( transaction.projectDir + "/" );
116✔
2411
    MerginProjectMetadata oldServerProject = MerginProjectMetadata::fromCachedJson( transaction.projectDir + "/" + sMetadataFile );
116✔
2412

2413
    // Cache mergin-config, since we are on the most recent version, it is sufficient to just read the local version
2414
    if ( transaction.configAllowed )
116✔
2415
    {
2416
      transaction.config = MerginConfig::fromFile( transaction.projectDir + "/" + MerginApi::sMerginConfigFile );
107✔
2417
    }
107✔
2418

2419
    transaction.diff = compareProjectFiles(
116✔
2420
                         oldServerProject.files,
116✔
2421
                         serverProject.files,
116✔
2422
                         localFiles,
2423
                         transaction.projectDir,
116✔
2424
                         transaction.configAllowed,
116✔
2425
                         transaction.config
116✔
2426
                       );
2427

2428
    CoreUtils::log( "push " + projectFullName, transaction.diff.dump() );
116✔
2429

2430
    // TODO: make sure there are no remote files to add/update/remove nor conflicts
2431

2432
    QList<MerginFile> filesToUpload;
116✔
2433
    QList<MerginFile> addedMerginFiles, updatedMerginFiles, deletedMerginFiles;
116✔
2434
    QList<MerginFile> diffFiles;
116✔
2435
    for ( QString filePath : transaction.diff.localAdded )
248✔
2436
    {
2437
      MerginFile merginFile = findFile( filePath, localFiles );
132✔
2438
      merginFile.chunks = generateChunkIdsForSize( merginFile.size );
132✔
2439
      addedMerginFiles.append( merginFile );
132✔
2440
    }
132✔
2441

2442
    for ( QString filePath : transaction.diff.localUpdated )
135✔
2443
    {
2444
      MerginFile merginFile = findFile( filePath, localFiles );
19✔
2445
      merginFile.chunks = generateChunkIdsForSize( merginFile.size );
19✔
2446

2447
      if ( MerginApi::isFileDiffable( filePath ) )
19✔
2448
      {
2449
        // try to create a diff
2450
        QString diffName;
12✔
2451
        int geodiffRes = GeodiffUtils::createChangeset( transaction.projectDir, filePath, diffName );
12✔
2452
        QString diffPath = transaction.projectDir + "/.mergin/" + diffName;
12✔
2453
        QString basePath = transaction.projectDir + "/.mergin/" + filePath;
12✔
2454

2455
        if ( geodiffRes == GEODIFF_SUCCESS )
12✔
2456
        {
2457
          QByteArray checksumDiff = getChecksum( diffPath );
12✔
2458

2459
          // TODO: this is ugly. our basefile may not need to have the same checksum as the server's
2460
          // basefile (because each of them have applied the diff independently) so we have to fake it
2461
          QByteArray checksumBase = serverProject.fileInfo( filePath ).checksum.toLatin1();
12✔
2462

2463
          merginFile.diffName = diffName;
12✔
2464
          merginFile.diffChecksum = QString::fromLatin1( checksumDiff.data(), checksumDiff.size() );
12✔
2465
          merginFile.diffSize = QFileInfo( diffPath ).size();
12✔
2466
          merginFile.chunks = generateChunkIdsForSize( merginFile.diffSize );
12✔
2467
          merginFile.diffBaseChecksum = QString::fromLatin1( checksumBase.data(), checksumBase.size() );
12✔
2468

2469
          diffFiles.append( merginFile );
12✔
2470

2471
          CoreUtils::log( "push " + projectFullName, QString( "Geodiff create changeset on %1 successful: total size %2 bytes" ).arg( filePath ).arg( merginFile.diffSize ) );
12✔
2472
        }
12✔
2473
        else
2474
        {
2475
          // TODO: remove the diff file (if exists)
2476
          CoreUtils::log( "push " + projectFullName, QString( "Geodiff create changeset on %1 FAILED with error %2 (will do full upload)" ).arg( filePath ).arg( geodiffRes ) );
×
2477
        }
2478
      }
12✔
2479

2480
      updatedMerginFiles.append( merginFile );
19✔
2481
    }
19✔
2482

2483
    for ( QString filePath : transaction.diff.localDeleted )
120✔
2484
    {
2485
      MerginFile merginFile = findFile( filePath, serverProject.files );
4✔
2486
      deletedMerginFiles.append( merginFile );
4✔
2487
    }
4✔
2488

2489
    if ( addedMerginFiles.isEmpty() && updatedMerginFiles.isEmpty() && deletedMerginFiles.isEmpty() )
116✔
2490
    {
2491
      // if nothing has changed, there is no point to even start upload transaction
2492
      transaction.projectMetadata = data;
1✔
2493
      transaction.version = MerginProjectMetadata::fromJson( data ).version;
1✔
2494

2495
      finishProjectSync( projectFullName, true );
1✔
2496
      return;
1✔
2497
    }
2498

2499
    QJsonArray added = prepareUploadChangesJSON( addedMerginFiles );
115✔
2500
    filesToUpload.append( addedMerginFiles );
115✔
2501

2502
    QJsonArray modified = prepareUploadChangesJSON( updatedMerginFiles );
115✔
2503
    filesToUpload.append( updatedMerginFiles );
115✔
2504

2505
    QJsonArray removed = prepareUploadChangesJSON( deletedMerginFiles );
115✔
2506
    // removed not in filesToUpload
2507

2508
    QJsonObject changes;
115✔
2509
    changes.insert( "added", added );
115✔
2510
    changes.insert( "removed", removed );
115✔
2511
    changes.insert( "updated", modified );
115✔
2512
    changes.insert( "renamed", QJsonArray() );
115✔
2513

2514
    qint64 totalSize = 0;
115✔
2515
    for ( MerginFile file : filesToUpload )
266✔
2516
    {
2517
      if ( !file.diffName.isEmpty() )
151✔
2518
        totalSize += file.diffSize;
12✔
2519
      else
2520
        totalSize += file.size;
139✔
2521
    }
151✔
2522

2523
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "%1 items to upload (total size %2 bytes)" )
230✔
2524
                    .arg( filesToUpload.count() ).arg( totalSize ) );
115✔
2525

2526
    transaction.totalSize = totalSize;
115✔
2527
    transaction.pushQueue = filesToUpload;
115✔
2528
    transaction.pushDiffFiles = diffFiles;
115✔
2529

2530
    QJsonObject json;
115✔
2531
    json.insert( QStringLiteral( "changes" ), changes );
115✔
2532
    json.insert( QStringLiteral( "version" ), QString( "v%1" ).arg( serverProject.version ) );
115✔
2533
    QJsonDocument jsonDoc;
115✔
2534
    jsonDoc.setObject( json );
115✔
2535

2536
    pushStart( projectFullName, jsonDoc.toJson( QJsonDocument::Compact ) );
115✔
2537
  }
142✔
2538
  else
2539
  {
2540
    QString serverMsg = extractServerErrorMsg( r->readAll() );
1✔
2541
    QString message = QStringLiteral( "Network API error: %1(): %2" ).arg( QStringLiteral( "projectInfo" ), r->errorString() );
1✔
2542
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) );
1✔
2543

2544
    int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
1✔
2545
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: pushInfo" ), httpCode, projectFullName );
1✔
2546

2547
    transaction.replyPushProjectInfo->deleteLater();
1✔
2548
    transaction.replyPushProjectInfo = nullptr;
1✔
2549

2550
    finishProjectSync( projectFullName, false );
1✔
2551
  }
1✔
2552
}
143✔
2553

2554
void MerginApi::pushFinishReplyFinished()
110✔
2555
{
2556
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
110✔
2557
  Q_ASSERT( r );
110✔
2558

2559
  QString projectFullName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ) ).toString();
110✔
2560

2561
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
110✔
2562
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
110✔
2563
  Q_ASSERT( r == transaction.replyPushFinish );
110✔
2564

2565
  if ( r->error() == QNetworkReply::NoError )
110✔
2566
  {
2567
    Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
110✔
2568
    QByteArray data = r->readAll();
110✔
2569
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Transaction finish accepted" ) );
110✔
2570

2571
    transaction.replyPushFinish->deleteLater();
110✔
2572
    transaction.replyPushFinish = nullptr;
110✔
2573

2574
    transaction.projectMetadata = data;
110✔
2575
    transaction.version = MerginProjectMetadata::fromJson( data ).version;
110✔
2576

2577
    //  a new diffable files suppose to have their basefile copies in .mergin
2578
    for ( QString filePath : transaction.diff.localAdded )
240✔
2579
    {
2580
      if ( MerginApi::isFileDiffable( filePath ) )
130✔
2581
      {
2582
        QString basefile = transaction.projectDir + "/.mergin/" + filePath;
11✔
2583
        createPathIfNotExists( basefile );
11✔
2584

2585
        QString sourcePath = transaction.projectDir + "/" + filePath;
11✔
2586
        if ( !QFile::copy( sourcePath, basefile ) )
11✔
2587
        {
2588
          CoreUtils::log( "push " + projectFullName, "failed to copy new basefile for: " + filePath );
×
2589
        }
×
2590
      }
11✔
2591
    }
130✔
2592

2593
    // clean up diff-related files
2594
    const auto diffFiles = transaction.pushDiffFiles;
110✔
2595
    for ( const MerginFile &merginFile : diffFiles )
122✔
2596
    {
2597
      QString diffPath = transaction.projectDir + "/.mergin/" + merginFile.diffName;
12✔
2598

2599
      // update basefile (unmodified file that should be equivalent to the server)
2600
      QString basePath = transaction.projectDir + "/.mergin/" + merginFile.path;
12✔
2601
      int res = GEODIFF_applyChangeset( basePath.toUtf8(), diffPath.toUtf8() );
12✔
2602
      if ( res == GEODIFF_SUCCESS )
12✔
2603
      {
2604
        CoreUtils::log( "push " + projectFullName, QString( "Applied %1 to base file of %2" ).arg( merginFile.diffName, merginFile.path ) );
12✔
2605
      }
12✔
2606
      else
2607
      {
2608
        CoreUtils::log( "push " + projectFullName, QString( "Failed to apply changeset %1 to basefile %2 - error %3" ).arg( diffPath ).arg( basePath ).arg( res ) );
×
2609
      }
2610

2611
      // remove temporary diff files
2612
      if ( !QFile::remove( diffPath ) )
12✔
2613
        CoreUtils::log( "push " + projectFullName, "Failed to remove diff: " + diffPath );
×
2614
    }
12✔
2615

2616
    finishProjectSync( projectFullName, true );
110✔
2617
  }
110✔
2618
  else
2619
  {
2620
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
2621
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "pushFinish" ), r->errorString(), serverMsg );
×
2622
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) );
×
2623

2624
    int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
×
2625
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: pushFinish" ), httpCode, projectFullName );
×
2626

2627
    // remove temporary diff files
2628
    const auto diffFiles = transaction.pushDiffFiles;
×
2629
    for ( const MerginFile &merginFile : diffFiles )
×
2630
    {
2631
      QString diffPath = transaction.projectDir + "/.mergin/" + merginFile.diffName;
×
2632
      if ( !QFile::remove( diffPath ) )
×
2633
        CoreUtils::log( "push " + projectFullName, "Failed to remove diff: " + diffPath );
×
2634
    }
×
2635

2636
    transaction.replyPushFinish->deleteLater();
×
2637
    transaction.replyPushFinish = nullptr;
×
2638

2639
    finishProjectSync( projectFullName, false );
×
2640
  }
×
2641
}
110✔
2642

2643
void MerginApi::pushCancelReplyFinished()
1✔
2644
{
2645
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
1✔
2646
  Q_ASSERT( r );
1✔
2647

2648
  QString projectFullName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ) ).toString();
1✔
2649

2650
  if ( r->error() == QNetworkReply::NoError )
1✔
2651
  {
2652
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Transaction canceled" ) );
1✔
2653
  }
1✔
2654
  else
2655
  {
2656
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
2657
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "uploadCancel" ), r->errorString(), serverMsg );
×
2658
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) );
×
2659
  }
×
2660

2661
  emit pushCanceled( projectFullName, r->error() == QNetworkReply::NoError );
1✔
2662

2663
  r->deleteLater();
1✔
2664
}
1✔
2665

2666
void MerginApi::getUserInfoFinished()
20✔
2667
{
2668
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
20✔
2669
  Q_ASSERT( r );
20✔
2670

2671
  if ( r->error() == QNetworkReply::NoError )
20✔
2672
  {
2673
    CoreUtils::log( "user info", QStringLiteral( "Success" ) );
20✔
2674
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
20✔
2675
    if ( doc.isObject() )
20✔
2676
    {
2677
      QJsonObject docObj = doc.object();
20✔
2678
      mUserInfo->setFromJson( docObj );
20✔
2679
      if ( mServerType == MerginServerType::OLD )
20✔
2680
      {
2681
        mWorkspaceInfo->setFromJson( docObj );
×
2682
      }
×
2683
    }
20✔
2684
  }
20✔
2685
  else
2686
  {
2687
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
2688
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "getUserInfo" ), r->errorString(), serverMsg );
×
2689
    CoreUtils::log( "user info", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
2690
    mUserInfo->clear();
×
2691
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: getUserInfo" ) );
×
2692
  }
×
2693

2694
  emit userInfoReplyFinished();
20✔
2695

2696
  r->deleteLater();
20✔
2697
}
20✔
2698

2699
void MerginApi::getWorkspaceInfoReplyFinished()
10✔
2700
{
2701
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
10✔
2702
  Q_ASSERT( r );
10✔
2703

2704
  if ( r->error() == QNetworkReply::NoError )
10✔
2705
  {
2706
    CoreUtils::log( "workspace info", QStringLiteral( "Success" ) );
10✔
2707
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
10✔
2708
    if ( doc.isObject() )
10✔
2709
    {
2710
      QJsonObject docObj = doc.object();
10✔
2711
      mWorkspaceInfo->setFromJson( docObj );
10✔
2712

2713
      emit getWorkspaceInfoFinished();
10✔
2714
    }
10✔
2715
  }
10✔
2716
  else
2717
  {
2718
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
2719
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "getWorkspaceInfo" ), r->errorString(), serverMsg );
×
2720
    CoreUtils::log( "workspace info", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
2721
    mWorkspaceInfo->clear();
×
2722
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: getWorkspaceInfo" ) );
×
2723
  }
×
2724

2725
  r->deleteLater();
10✔
2726
}
10✔
2727

2728
ProjectDiff MerginApi::compareProjectFiles(
281✔
2729
  const QList<MerginFile> &oldServerFiles,
2730
  const QList<MerginFile> &newServerFiles,
2731
  const QList<MerginFile> &localFiles,
2732
  const QString &projectDir,
2733
  bool allowConfig,
2734
  const MerginConfig &config,
2735
  const MerginConfig &lastSyncConfig
2736
)
2737
{
2738
  ProjectDiff diff;
281✔
2739
  QHash<QString, MerginFile> oldServerFilesMap, newServerFilesMap;
281✔
2740

2741
  for ( MerginFile file : newServerFiles )
1,481✔
2742
  {
2743
    newServerFilesMap.insert( file.path, file );
1,200✔
2744
  }
1,200✔
2745
  for ( MerginFile file : oldServerFiles )
1,275✔
2746
  {
2747
    oldServerFilesMap.insert( file.path, file );
994✔
2748
  }
994✔
2749

2750
  for ( MerginFile localFile : localFiles )
1,274✔
2751
  {
2752
    QString filePath = localFile.path;
993✔
2753
    bool hasOldServer = oldServerFilesMap.contains( localFile.path );
993✔
2754
    bool hasNewServer = newServerFilesMap.contains( localFile.path );
993✔
2755
    QString chkOld = oldServerFilesMap.value( localFile.path ).checksum;
993✔
2756
    QString chkNew = newServerFilesMap.value( localFile.path ).checksum;
993✔
2757
    QString chkLocal = localFile.checksum;
993✔
2758

2759
    if ( !hasOldServer && !hasNewServer )
993✔
2760
    {
2761
      // L-A
2762
      diff.localAdded << filePath;
177✔
2763
    }
177✔
2764
    else if ( hasOldServer && !hasNewServer )
816✔
2765
    {
2766
      if ( chkOld == chkLocal )
4✔
2767
      {
2768
        // R-D
2769
        diff.remoteDeleted << filePath;
3✔
2770
      }
3✔
2771
      else
2772
      {
2773
        // C/R-D/L-U
2774
        diff.conflictRemoteDeletedLocalUpdated << filePath;
1✔
2775
      }
2776
    }
4✔
2777
    else if ( !hasOldServer && hasNewServer )
812✔
2778
    {
2779
      if ( chkNew != chkLocal )
1✔
2780
      {
2781
        // C/R-A/L-A
2782
        diff.conflictRemoteAddedLocalAdded << filePath;
1✔
2783
      }
1✔
2784
      else
2785
      {
2786
        // R-A/L-A
2787
        // TODO: need to do anything?
2788
      }
2789
    }
1✔
2790
    else if ( hasOldServer && hasNewServer )
811✔
2791
    {
2792
      // file has already existed
2793
      if ( chkOld == chkNew )
811✔
2794
      {
2795
        if ( chkNew != chkLocal )
792✔
2796
        {
2797
          // L-U
2798
          if ( isFileDiffable( filePath ) )
37✔
2799
          {
2800
            // we need to do a diff here to figure out whether the file is actually changed or not
2801
            // because the real content may be the same although the checksums do not match
2802
            if ( GeodiffUtils::hasPendingChanges( projectDir, filePath ) )
29✔
2803
              diff.localUpdated << filePath;
17✔
2804
          }
29✔
2805
          else
2806
            diff.localUpdated << filePath;
8✔
2807
        }
37✔
2808
        else
2809
        {
2810
          // no change :-)
2811
        }
2812
      }
792✔
2813
      else   // v1 != v2
2814
      {
2815
        if ( chkNew != chkLocal && chkOld != chkLocal )
19✔
2816
        {
2817
          // C/R-U/L-U
2818
          if ( isFileDiffable( filePath ) )
7✔
2819
          {
2820
            // we need to do a diff here to figure out whether the file is actually changed or not
2821
            // because the real content may be the same although the checksums do not match
2822
            if ( GeodiffUtils::hasPendingChanges( projectDir, filePath ) )
5✔
2823
              diff.conflictRemoteUpdatedLocalUpdated << filePath;
3✔
2824
            else
2825
              diff.remoteUpdated << filePath;
2✔
2826
          }
5✔
2827
          else
2828
            diff.conflictRemoteUpdatedLocalUpdated << filePath;
2✔
2829
        }
7✔
2830
        else if ( chkNew != chkLocal )  // && old == local
12✔
2831
        {
2832
          // R-U
2833
          diff.remoteUpdated << filePath;
12✔
2834
        }
12✔
2835
        else if ( chkOld != chkLocal )  // && new == local
×
2836
        {
2837
          // R-U/L-U
2838
          // TODO: need to do anything?
2839
        }
×
2840
        else
2841
          Q_ASSERT( false );   // impossible - should be handled already
×
2842
      }
2843
    }
811✔
2844

2845
    if ( hasOldServer )
993✔
2846
      oldServerFilesMap.remove( filePath );
815✔
2847
    if ( hasNewServer )
993✔
2848
      newServerFilesMap.remove( filePath );
812✔
2849
  }
993✔
2850

2851
  // go through files listed on the server, but not available locally
2852
  for ( MerginFile file : newServerFilesMap )
669✔
2853
  {
2854
    bool hasOldServer = oldServerFilesMap.contains( file.path );
388✔
2855

2856
    if ( hasOldServer )
388✔
2857
    {
2858
      if ( oldServerFilesMap.value( file.path ).checksum == file.checksum )
179✔
2859
      {
2860
        // L-D
2861
        if ( allowConfig )
179✔
2862
        {
2863
          bool shouldBeExcludedFromSync = MerginApi::excludeFromSync( file.path, config );
177✔
2864
          if ( shouldBeExcludedFromSync )
177✔
2865
          {
2866
            continue;
155✔
2867
          }
2868

2869
          // check if we should download missing files that were previously ignored (e.g. selective sync has been disabled)
2870
          bool previouslyIgnoredButShouldDownload = \
22✔
2871
              config.downloadMissingFiles &&
22✔
2872
              lastSyncConfig.isValid &&
19✔
2873
              MerginApi::excludeFromSync( file.path, lastSyncConfig );
19✔
2874

2875
          if ( previouslyIgnoredButShouldDownload )
22✔
2876
          {
2877
            diff.remoteAdded << file.path;
19✔
2878
            continue;
19✔
2879
          }
2880
        }
3✔
2881
        diff.localDeleted << file.path;
5✔
2882
      }
5✔
2883
      else
2884
      {
2885
        // C/R-U/L-D
2886
        diff.conflictRemoteUpdatedLocalDeleted << file.path;
×
2887
      }
2888
    }
5✔
2889
    else
2890
    {
2891
      // R-A
2892
      if ( allowConfig )
209✔
2893
      {
2894
        if ( MerginApi::excludeFromSync( file.path, config ) )
167✔
2895
        {
2896
          continue;
35✔
2897
        }
2898
      }
132✔
2899
      diff.remoteAdded << file.path;
174✔
2900
    }
2901

2902
    if ( hasOldServer )
179✔
2903
      oldServerFilesMap.remove( file.path );
5✔
2904
  }
388✔
2905

2906
  for ( MerginFile file : oldServerFilesMap )
455✔
2907
  {
2908
    // R-D/L-D
2909
    // TODO: need to do anything?
2910
  }
174✔
2911

2912
  return diff;
281✔
2913
}
281✔
2914

2915
MerginProject MerginApi::parseProjectMetadata( const QJsonObject &proj )
286✔
2916
{
2917
  MerginProject project;
286✔
2918

2919
  if ( proj.isEmpty() )
286✔
2920
  {
2921
    return project;
×
2922
  }
2923

2924
  if ( proj.contains( QStringLiteral( "error" ) ) )
286✔
2925
  {
2926
    // handle project error (user might be logged out / do not have write rights / project is on different server / project is orphaned)
2927
    project.remoteError = QString::number( proj.value( QStringLiteral( "error" ) ).toInt( 0 ) ); // error code
×
2928
    return project;
×
2929
  }
2930

2931
  project.projectName = proj.value( QStringLiteral( "name" ) ).toString();
286✔
2932
  project.projectNamespace = proj.value( QStringLiteral( "namespace" ) ).toString();
286✔
2933

2934
  QString versionStr = proj.value( QStringLiteral( "version" ) ).toString();
286✔
2935
  if ( versionStr.isEmpty() )
286✔
2936
  {
2937
    project.serverVersion = 0;
×
2938
  }
×
2939
  else if ( versionStr.startsWith( "v" ) ) // cut off 'v' part from v123
286✔
2940
  {
2941
    versionStr = versionStr.mid( 1 );
286✔
2942
    project.serverVersion = versionStr.toInt();
286✔
2943
  }
286✔
2944

2945
  QDateTime updated = QDateTime::fromString( proj.value( QStringLiteral( "updated" ) ).toString(), Qt::ISODateWithMs ).toUTC();
286✔
2946
  if ( !updated.isValid() )
286✔
2947
  {
2948
    project.serverUpdated = QDateTime::fromString( proj.value( QStringLiteral( "created" ) ).toString(), Qt::ISODateWithMs ).toUTC();
×
2949
  }
×
2950
  else
2951
  {
2952
    project.serverUpdated = updated;
286✔
2953
  }
2954
  return project;
286✔
2955
}
286✔
2956

2957

2958
MerginProjectsList MerginApi::parseProjectsFromJson( const QJsonDocument &doc )
29✔
2959
{
2960
  if ( !doc.isObject() )
29✔
2961
    return MerginProjectsList();
×
2962

2963
  QJsonObject object = doc.object();
29✔
2964
  MerginProjectsList result;
29✔
2965

2966
  if ( object.contains( "projects" ) && object.value( "projects" ).isArray() ) // listProjects API
51✔
2967
  {
2968
    QJsonArray vArray = object.value( "projects" ).toArray();
22✔
2969

2970
    for ( auto it = vArray.constBegin(); it != vArray.constEnd(); ++it )
195✔
2971
    {
2972
      result << parseProjectMetadata( it->toObject() );
173✔
2973
    }
173✔
2974
  }
22✔
2975
  else if ( !object.isEmpty() ) // listProjectsbyName API returns projects as separate objects not in array
7✔
2976
  {
2977
    for ( auto it = object.begin(); it != object.end(); ++it )
120✔
2978
    {
2979
      MerginProject project = parseProjectMetadata( it->toObject() );
113✔
2980
      if ( !project.remoteError.isEmpty() )
113✔
2981
      {
2982
        // add project namespace/name from object name in case of error
2983
        MerginApi::extractProjectName( it.key(), project.projectNamespace, project.projectName );
×
2984
      }
×
2985
      result << project;
113✔
2986
    }
113✔
2987
  }
7✔
2988
  return result;
29✔
2989
}
29✔
2990

2991
void MerginApi::refreshAuthToken()
7✔
2992
{
2993
  if ( !mUserAuth->hasAuthData() ||
7✔
2994
       mUserAuth->authToken().isEmpty() )
7✔
2995
  {
2996
    CoreUtils::log( QStringLiteral( "Auth" ), QStringLiteral( "Can not refresh token, missing credentials" ) );
×
2997
    return;
×
2998
  }
2999

3000
  if ( mUserAuth->tokenExpiration() < QDateTime::currentDateTimeUtc() )
7✔
3001
  {
3002
    CoreUtils::log( QStringLiteral( "Auth" ), QStringLiteral( "Token has expired, requesting new one" ) );
×
3003
    authorize( mUserAuth->username(), mUserAuth->password() );
×
3004
    mAuthLoopEvent.exec();
×
3005
  }
×
3006
}
7✔
3007

3008
QStringList MerginApi::generateChunkIdsForSize( qint64 fileSize )
163✔
3009
{
3010
  qreal rawNoOfChunks = qreal( fileSize ) / UPLOAD_CHUNK_SIZE;
163✔
3011
  int noOfChunks = qCeil( rawNoOfChunks );
163✔
3012

3013
  // edge case when file is empty, filesize equals zero
3014
  // manually set one chunk so that file will be synced
3015
  if ( fileSize <= 0 )
163✔
3016
    noOfChunks = 1;
45✔
3017

3018
  QStringList chunks;
163✔
3019
  for ( int i = 0; i < noOfChunks; i++ )
328✔
3020
  {
3021
    QString chunkID = CoreUtils::uuidWithoutBraces( QUuid::createUuid() );
165✔
3022
    chunks.append( chunkID );
165✔
3023
  }
165✔
3024
  return chunks;
163✔
3025
}
163✔
3026

3027
QJsonArray MerginApi::prepareUploadChangesJSON( const QList<MerginFile> &files )
345✔
3028
{
3029
  QJsonArray jsonArray;
345✔
3030

3031
  for ( MerginFile file : files )
500✔
3032
  {
3033
    QJsonObject fileObject;
155✔
3034
    fileObject.insert( "path", file.path );
155✔
3035

3036
    fileObject.insert( "size", file.size );
155✔
3037
    fileObject.insert( "mtime", file.mtime.toString( Qt::ISODateWithMs ) );
155✔
3038

3039
    if ( !file.diffName.isEmpty() )
155✔
3040
    {
3041
      // doing diff-based upload
3042
      QJsonObject diffObject;
12✔
3043
      diffObject.insert( "path", file.diffName );
12✔
3044
      diffObject.insert( "checksum", file.diffChecksum );
12✔
3045
      diffObject.insert( "size", file.diffSize );
12✔
3046

3047
      fileObject.insert( "diff", diffObject );
12✔
3048
      fileObject.insert( "checksum", file.diffBaseChecksum );
12✔
3049
    }
12✔
3050
    else
3051
    {
3052
      fileObject.insert( "checksum", file.checksum );
143✔
3053
    }
3054

3055
    QJsonArray chunksJson;
155✔
3056
    for ( QString id : file.chunks )
308✔
3057
    {
3058
      chunksJson.append( id );
153✔
3059
    }
153✔
3060
    fileObject.insert( "chunks", chunksJson );
155✔
3061
    jsonArray.append( fileObject );
155✔
3062
  }
155✔
3063
  return jsonArray;
345✔
3064
}
345✔
3065

3066
void MerginApi::finishProjectSync( const QString &projectFullName, bool syncSuccessful )
243✔
3067
{
3068
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
243✔
3069
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
243✔
3070

3071
  emit syncProjectStatusChanged( projectFullName, -1 );   // -1 means there's no sync going on
243✔
3072

3073
  if ( syncSuccessful )
243✔
3074
  {
3075
    // update the local metadata file
3076
    writeData( transaction.projectMetadata, transaction.projectDir + "/" + MerginApi::sMetadataFile );
238✔
3077

3078
    // update info of local projects
3079
    mLocalProjects.updateLocalVersion( transaction.projectDir, transaction.version );
238✔
3080

3081
    CoreUtils::log( "sync " + projectFullName, QStringLiteral( "### Finished ###  New project version: %1\n" ).arg( transaction.version ) );
238✔
3082
  }
238✔
3083
  else
3084
  {
3085
    CoreUtils::log( "sync " + projectFullName, QStringLiteral( "### FAILED ###\n" ) );
5✔
3086
  }
3087

3088
  bool pullBeforePush = transaction.pullBeforePush;
243✔
3089
  QString projectDir = transaction.projectDir;  // keep it before the transaction gets removed
243✔
3090
  ProjectDiff diff = transaction.diff;
243✔
3091
  int newVersion = syncSuccessful ? transaction.version : -1;
243✔
3092
  mTransactionalStatus.remove( projectFullName );
243✔
3093

3094
  if ( transaction.gpkgSchemaChanged )
243✔
3095
  {
3096
    emit projectReloadNeededAfterSync( projectFullName );
96✔
3097
  }
96✔
3098

3099
  if ( pullBeforePush )
243✔
3100
  {
3101
    CoreUtils::log( "sync " + projectFullName, QStringLiteral( "Continue with push after pull" ) );
26✔
3102
    // we're done only with the download part before the actual upload - so let's continue with upload
3103
    QString projectNamespace, projectName;
26✔
3104
    extractProjectName( projectFullName, projectNamespace, projectName );
26✔
3105
    pushProject( projectNamespace, projectName );
26✔
3106
  }
26✔
3107
  else
3108
  {
3109
    emit syncProjectFinished( projectFullName, syncSuccessful, newVersion );
217✔
3110

3111
    if ( syncSuccessful )
217✔
3112
    {
3113
      emit projectDataChanged( projectFullName );
212✔
3114
    }
212✔
3115
  }
3116
}
243✔
3117

3118
bool MerginApi::writeData( const QByteArray &data, const QString &path )
238✔
3119
{
3120
  QFile file( path );
238✔
3121
  createPathIfNotExists( path );
238✔
3122
  if ( !file.open( QIODevice::WriteOnly ) )
238✔
3123
  {
3124
    return false;
×
3125
  }
3126

3127
  file.write( data );
238✔
3128
  file.close();
238✔
3129

3130
  return true;
238✔
3131
}
238✔
3132

3133

3134
void MerginApi::createPathIfNotExists( const QString &filePath )
706✔
3135
{
3136
  QDir dir;
706✔
3137
  if ( !dir.exists( mDataDir ) )
706✔
3138
    dir.mkpath( mDataDir );
×
3139

3140
  QFileInfo newFile( filePath );
706✔
3141
  if ( !newFile.absoluteDir().exists() )
706✔
3142
  {
3143
    if ( !dir.mkpath( newFile.absolutePath() ) )
196✔
3144
    {
3145
      CoreUtils::log( "create path", QString( "Creating a folder failed for path: %1" ).arg( filePath ) );
×
3146
    }
×
3147
  }
196✔
3148
}
706✔
3149

3150
bool MerginApi::isInIgnore( const QFileInfo &info )
2,461✔
3151
{
3152
  return sIgnoreExtensions.contains( info.suffix() ) || sIgnoreFiles.contains( info.fileName() ) || info.filePath().contains( sMetadataFolder + "/" );
2,461✔
3153
}
×
3154

3155
bool MerginApi::excludeFromSync( const QString &filePath, const MerginConfig &config )
377✔
3156
{
3157
  if ( config.isValid && config.selectiveSyncEnabled )
377✔
3158
  {
3159
    QFileInfo info( filePath );
247✔
3160

3161
    bool isExcludedFormat = sIgnoreImageExtensions.contains( info.suffix().toLower() );
247✔
3162

3163
    if ( !isExcludedFormat )
247✔
3164
      return false;
20✔
3165

3166
    if ( config.selectiveSyncDir.isEmpty() )
227✔
3167
    {
3168
      return true; // we are ignoring photos in the entire project
98✔
3169
    }
3170
    else if ( filePath.startsWith( config.selectiveSyncDir ) )
129✔
3171
    {
3172
      return true; // we are ignoring photo in subfolder
121✔
3173
    }
3174
  }
247✔
3175
  return false;
138✔
3176
}
377✔
3177

3178
QByteArray MerginApi::getChecksum( const QString &filePath )
1,009✔
3179
{
3180
  QFile f( filePath );
1,009✔
3181
  if ( f.open( QFile::ReadOnly ) )
1,009✔
3182
  {
3183
    QCryptographicHash hash( QCryptographicHash::Sha1 );
1,009✔
3184
    QByteArray chunk = f.read( CHUNK_SIZE );
1,009✔
3185
    while ( !chunk.isEmpty() )
2,596✔
3186
    {
3187
      hash.addData( chunk );
1,587✔
3188
      chunk = f.read( CHUNK_SIZE );
1,587✔
3189
    }
3190
    f.close();
1,009✔
3191
    return hash.result().toHex();
1,009✔
3192
  }
1,009✔
3193

3194
  return QByteArray();
×
3195
}
1,009✔
3196

3197
QSet<QString> MerginApi::listFiles( const QString &path )
281✔
3198
{
3199
  QSet<QString> files;
281✔
3200
  QDirIterator it( path, QStringList() << QStringLiteral( "*" ), QDir::Files, QDirIterator::Subdirectories );
281✔
3201
  while ( it.hasNext() )
1,274✔
3202
  {
3203
    it.next();
993✔
3204
    if ( !isInIgnore( it.fileInfo() ) )
993✔
3205
    {
3206
      files << it.filePath().replace( path, "" );
993✔
3207
    }
993✔
3208
  }
3209
  return files;
281✔
3210
}
281✔
3211

3212
void MerginApi::deleteAccount()
1✔
3213
{
3214
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
1✔
3215
  {
3216
    return;
×
3217
  }
3218

3219
  QNetworkRequest request = getDefaultRequest();
1✔
3220
  QUrl url( mApiRoot + QStringLiteral( "/v1/user" ) );
1✔
3221
  request.setUrl( url );
1✔
3222
  QNetworkReply *reply = mManager.deleteResource( request );
1✔
3223
  connect( reply, &QNetworkReply::finished, this, [this]() { this->deleteAccountFinished();} );
2✔
3224
  CoreUtils::log( "delete account " + mUserAuth->username(), QStringLiteral( "Requesting account deletion: " ) + url.toString() );
1✔
3225
}
1✔
3226

3227
void MerginApi::deleteAccountFinished()
1✔
3228
{
3229
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
1✔
3230
  Q_ASSERT( r );
1✔
3231

3232
  if ( r->error() == QNetworkReply::NoError )
1✔
3233
  {
3234
    CoreUtils::log( "delete account " + mUserAuth->username(), QStringLiteral( "Success" ) );
1✔
3235

3236
    // remove all local projects from the device
3237
    LocalProjectsList projects = mLocalProjects.projects();
1✔
3238
    for ( const LocalProject &info : projects )
36✔
3239
    {
3240
      mLocalProjects.removeLocalProject( info.id() );
35✔
3241
    }
3242

3243
    clearAuth();
1✔
3244

3245
    emit accountDeleted( true );
1✔
3246
  }
1✔
3247
  else
3248
  {
3249
    int statusCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
×
3250
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
3251
    CoreUtils::log( "delete account " + mUserAuth->username(), QStringLiteral( "FAILED - %1 %2. %3" ).arg( statusCode ).arg( r->errorString() ).arg( serverMsg ) );
×
3252
    if ( statusCode == 422 )
×
3253
    {
3254
      emit userIsAnOrgOwnerError();
×
3255
    }
×
3256
    else
3257
    {
3258
      emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: deleteAccount" ) );
×
3259
    }
3260

3261
    emit accountDeleted( false );
×
3262
  }
×
3263

3264
  r->deleteLater();
1✔
3265
}
1✔
3266

3267
void MerginApi::getServerConfig()
24✔
3268
{
3269
  QNetworkRequest request = getDefaultRequest();
24✔
3270
  QString urlString = mApiRoot + QStringLiteral( "/config" );
24✔
3271
  QUrl url( urlString );
24✔
3272
  request.setUrl( url );
24✔
3273

3274
  QNetworkReply *reply = mManager.get( request );
24✔
3275

3276
  connect( reply, &QNetworkReply::finished, this, &MerginApi::getServerConfigReplyFinished );
24✔
3277
  CoreUtils::log( "Config", QStringLiteral( "Requesting server configuration: " ) + url.toString() );
24✔
3278
}
24✔
3279

3280
void MerginApi::getServerConfigReplyFinished()
12✔
3281
{
3282
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
12✔
3283
  Q_ASSERT( r );
12✔
3284

3285
  if ( r->error() == QNetworkReply::NoError )
12✔
3286
  {
3287
    CoreUtils::log( "Config", QStringLiteral( "Success" ) );
12✔
3288
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
12✔
3289
    if ( doc.isObject() )
12✔
3290
    {
3291
      QString serverType = doc.object().value( QStringLiteral( "server_type" ) ).toString();
12✔
3292
      if ( serverType == QStringLiteral( "ee" ) )
12✔
3293
      {
3294
        setServerType( MerginServerType::EE );
×
3295
      }
×
3296
      else if ( serverType == QStringLiteral( "ce" ) )
12✔
3297
      {
3298
        setServerType( MerginServerType::CE );
×
3299
      }
×
3300
      else if ( serverType == QStringLiteral( "saas" ) )
12✔
3301
      {
3302
        setServerType( MerginServerType::SAAS );
12✔
3303
      }
12✔
3304
    }
12✔
3305
  }
12✔
3306
  else
3307
  {
3308
    int statusCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
×
3309
    if ( statusCode == 404 ) // legacy (old) server
×
3310
    {
3311
      setServerType( MerginServerType::OLD );
×
3312
    }
×
3313
    else
3314
    {
3315
      QString serverMsg = extractServerErrorMsg( r->readAll() );
×
3316
      QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "getServerType" ), r->errorString(), serverMsg );
×
3317
      emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: getServerType" ) );
×
3318
      CoreUtils::log( "server type", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
3319
    }
×
3320
  }
3321

3322
  r->deleteLater();
12✔
3323
}
12✔
3324

3325
MerginServerType::ServerType MerginApi::serverType() const
6✔
3326
{
3327
  return mServerType;
6✔
3328
}
3329

3330
void MerginApi::setServerType( const MerginServerType::ServerType &serverType )
17✔
3331
{
3332
  if ( mServerType != serverType )
17✔
3333
  {
3334
    if ( mServerType == MerginServerType::OLD && serverType == MerginServerType::SAAS )
6✔
3335
    {
3336
      emit serverWasUpgraded();
2✔
3337
    }
2✔
3338

3339
    mServerType = serverType;
6✔
3340
    QSettings settings;
6✔
3341
    settings.beginGroup( QStringLiteral( "Input/" ) );
6✔
3342
    settings.setValue( QStringLiteral( "serverType" ), mServerType );
6✔
3343
    settings.endGroup();
6✔
3344
    emit serverTypeChanged();
6✔
3345
    emit apiSupportsWorkspacesChanged();
6✔
3346
  }
6✔
3347
}
17✔
3348

3349
void MerginApi::listWorkspaces()
×
3350
{
3351
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
×
3352
  {
3353
    emit listWorkspacesFailed();
×
3354
    return;
×
3355
  }
3356

3357
  QUrl url( mApiRoot + QStringLiteral( "/v1/workspaces" ) );
×
3358
  QNetworkRequest request = getDefaultRequest( mUserAuth->hasAuthData() );
×
3359
  request.setUrl( url );
×
3360

3361
  QNetworkReply *reply = mManager.get( request );
×
3362
  CoreUtils::log( "list workspaces", QStringLiteral( "Requesting: " ) + url.toString() );
×
3363
  connect( reply, &QNetworkReply::finished, this, &MerginApi::listWorkspacesReplyFinished );
×
3364
}
×
3365

3366
void MerginApi::listWorkspacesReplyFinished()
×
3367
{
3368
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
×
3369
  Q_ASSERT( r );
×
3370

3371
  if ( r->error() == QNetworkReply::NoError )
×
3372
  {
3373
    CoreUtils::log( "list workspaces", QStringLiteral( "Success" ) );
×
3374
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
×
3375
    if ( doc.isArray() )
×
3376
    {
3377
      QMap<int, QString> workspaces;
×
3378
      QJsonArray array = doc.array();
×
3379
      for ( auto it = array.constBegin(); it != array.constEnd(); ++it )
×
3380
      {
3381
        QJsonObject ws = it->toObject();
×
3382
        workspaces.insert( ws.value( QStringLiteral( "id" ) ).toInt(), ws.value( QStringLiteral( "name" ) ).toString() );
×
3383
      }
×
3384

3385
      mUserInfo->setWorkspaces( workspaces );
×
3386
      emit listWorkspacesFinished( workspaces );
×
3387
    }
×
3388
    else
3389
    {
3390
      emit listWorkspacesFailed();
×
3391
    }
3392
  }
×
3393
  else
3394
  {
3395
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
3396
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listWorkspaces" ), r->errorString(), serverMsg );
×
3397
    CoreUtils::log( "list workspaces", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
3398
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: listWorkspaces" ) );
×
3399
    emit listWorkspacesFailed();
×
3400
  }
×
3401

3402
  r->deleteLater();
×
3403
}
×
3404

3405
void MerginApi::listInvitations()
×
3406
{
3407
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
×
3408
  {
3409
    emit listInvitationsFailed();
×
3410
    return;
×
3411
  }
3412

3413
  QUrl url( mApiRoot + QStringLiteral( "/v1/workspace/invitations" ) );
×
3414
  QNetworkRequest request = getDefaultRequest( mUserAuth->hasAuthData() );
×
3415
  request.setUrl( url );
×
3416

3417
  QNetworkReply *reply = mManager.get( request );
×
3418
  CoreUtils::log( "list invitations", QStringLiteral( "Requesting: " ) + url.toString() );
×
3419
  connect( reply, &QNetworkReply::finished, this, &MerginApi::listInvitationsReplyFinished );
×
3420
}
×
3421

3422
void MerginApi::listInvitationsReplyFinished()
×
3423
{
3424
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
×
3425
  Q_ASSERT( r );
×
3426

3427
  if ( r->error() == QNetworkReply::NoError )
×
3428
  {
3429
    CoreUtils::log( "list invitations", QStringLiteral( "Success" ) );
×
3430
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
×
3431
    if ( doc.isArray() )
×
3432
    {
3433
      QList<MerginInvitation> invitations;
×
3434
      QJsonArray array = doc.array();
×
3435
      for ( auto it = array.constBegin(); it != array.constEnd(); ++it )
×
3436
      {
3437
        MerginInvitation invite = MerginInvitation::fromJsonObject( it->toObject() );
×
3438
        invitations.append( invite );
×
3439
      }
×
3440

3441
      emit listInvitationsFinished( invitations );
×
3442
    }
×
3443
    else
3444
    {
3445
      emit listInvitationsFailed();
×
3446
    }
3447
  }
×
3448
  else
3449
  {
3450
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
3451
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listInvitations" ), r->errorString(), serverMsg );
×
3452
    CoreUtils::log( "list invitations", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
3453
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: listInvitations" ) );
×
3454
    emit listInvitationsFailed();
×
3455
  }
×
3456

3457
  r->deleteLater();
×
3458
}
×
3459

3460
void MerginApi::processInvitation( const QString &uuid, bool accept )
×
3461
{
3462
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
×
3463
  {
3464
    emit processInvitationFailed();
×
3465
    return;
×
3466
  }
3467

3468
  QNetworkRequest request = getDefaultRequest( true );
×
3469
  QString urlString = mApiRoot + QStringLiteral( "v1/workspace/invitation/%1" ).arg( uuid );
×
3470
  QUrl url( urlString );
×
3471
  request.setUrl( url );
×
3472
  request.setRawHeader( "Content-Type", "application/json" );
×
3473
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrAcceptFlag ), accept );
×
3474

3475
  QJsonDocument jsonDoc;
×
3476
  QJsonObject jsonObject;
×
3477
  jsonObject.insert( QStringLiteral( "accept" ), accept );
×
3478
  jsonDoc.setObject( jsonObject );
×
3479
  QByteArray json = jsonDoc.toJson( QJsonDocument::Compact );
×
3480
  QNetworkReply *reply = mManager.post( request, json );
×
3481
  CoreUtils::log( "process invitation", QStringLiteral( "Requesting: " ) + url.toString() );
×
3482
  connect( reply, &QNetworkReply::finished, this, &MerginApi::processInvitationReplyFinished );
×
3483
}
×
3484

3485
void MerginApi::processInvitationReplyFinished()
×
3486
{
3487
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
×
3488
  Q_ASSERT( r );
×
3489

3490
  bool accept = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrAcceptFlag ) ).toBool();
×
3491

3492
  if ( r->error() == QNetworkReply::NoError )
×
3493
  {
3494
    CoreUtils::log( "process invitation", QStringLiteral( "Success" ) );
×
3495
  }
×
3496
  else
3497
  {
3498
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
3499
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "processInvitation" ), r->errorString(), serverMsg );
×
3500
    CoreUtils::log( "process invitation", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
3501
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: processInvitation" ) );
×
3502
    emit processInvitationFailed();
×
3503
  }
×
3504

3505
  emit processInvitationFinished( accept );
×
3506

3507
  r->deleteLater();
×
3508
}
×
3509

3510
bool MerginApi::createWorkspace( const QString &workspaceName )
2✔
3511
{
3512
  if ( !validateAuth() )
2✔
3513
  {
3514
    emit missingAuthorizationError( workspaceName );
×
3515
    return false;
×
3516
  }
3517

3518
  if ( mApiVersionStatus != MerginApiStatus::OK )
2✔
3519
  {
3520
    return false;
×
3521
  }
3522

3523
  if ( !CoreUtils::isValidName( workspaceName ) )
2✔
3524
  {
3525
    emit notify( tr( "Workspace name contains invalid characters" ) );
×
3526
    return false;
×
3527
  }
3528

3529
  QNetworkRequest request = getDefaultRequest();
2✔
3530
  QUrl url( mApiRoot + QString( "/v1/workspace" ) );
2✔
3531
  request.setUrl( url );
2✔
3532
  request.setRawHeader( "Content-Type", "application/json" );
2✔
3533
  request.setRawHeader( "Accept", "application/json" );
2✔
3534
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrWorkspaceName ), workspaceName );
2✔
3535

3536
  QJsonDocument jsonDoc;
2✔
3537
  QJsonObject jsonObject;
2✔
3538
  jsonObject.insert( QStringLiteral( "name" ), workspaceName );
2✔
3539
  jsonDoc.setObject( jsonObject );
2✔
3540
  QByteArray json = jsonDoc.toJson( QJsonDocument::Compact );
2✔
3541

3542
  QNetworkReply *reply = mManager.post( request, json );
2✔
3543
  connect( reply, &QNetworkReply::finished, this, &MerginApi::createWorkspaceReplyFinished );
2✔
3544
  CoreUtils::log( "create " + workspaceName, QStringLiteral( "Requesting workspace creation: " ) + url.toString() );
2✔
3545

3546
  return true;
2✔
3547
}
2✔
3548

3549
void MerginApi::signOut()
×
3550
{
3551
  clearAuth();
×
3552
}
×
3553

3554
void MerginApi::refreshUserData()
×
3555
{
3556
  getUserInfo();
×
3557

3558
  if ( apiSupportsWorkspaces() )
×
3559
  {
3560
    getWorkspaceInfo();
×
3561
    // getServiceInfo is called automatically when workspace info finishes
3562
  }
×
3563
  else if ( mServerType == MerginServerType::OLD )
×
3564
  {
3565
    getServiceInfo();
×
3566
  }
×
3567
}
×
3568

3569
void MerginApi::createWorkspaceReplyFinished()
2✔
3570
{
3571
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
2✔
3572
  Q_ASSERT( r );
2✔
3573

3574
  QString workspaceName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrWorkspaceName ) ).toString();
2✔
3575

3576
  if ( r->error() == QNetworkReply::NoError )
2✔
3577
  {
3578
    CoreUtils::log( "create " + workspaceName, QStringLiteral( "Success" ) );
2✔
3579
    emit workspaceCreated( workspaceName, true );
2✔
3580
  }
2✔
3581
  else
3582
  {
3583
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
3584
    QString message = QStringLiteral( "FAILED - %1: %2" ).arg( r->errorString(), serverMsg );
×
3585
    CoreUtils::log( "create " + workspaceName, message );
×
3586

3587
    int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
×
3588

3589
    if ( httpCode == 409 )
×
3590
    {
3591
      emit networkErrorOccurred( tr( "Workspace %1 already exists" ).arg( workspaceName ), QStringLiteral( "Mergin API error: createWorkspace" ), httpCode, workspaceName );
×
3592
    }
×
3593
    else
3594
    {
3595
      emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: createWorkspace" ), httpCode, workspaceName );
×
3596
    }
3597
    emit workspaceCreated( workspaceName, false );
×
3598
  }
×
3599
  r->deleteLater();
2✔
3600
}
2✔
3601

3602
bool MerginApi::apiSupportsWorkspaces()
×
3603
{
3604
  if ( mServerType == MerginServerType::SAAS || mServerType == MerginServerType::EE )
×
3605
  {
3606
    return true;
×
3607
  }
3608
  else
3609
  {
3610
    return false;
×
3611
  }
3612
}
×
3613

3614
DownloadQueueItem::DownloadQueueItem( const QString &fp, int s, int v, int rf, int rt, bool diff )
459✔
3615
  : filePath( fp ), size( s ), version( v ), rangeFrom( rf ), rangeTo( rt ), downloadDiff( diff )
153✔
3616
{
153✔
3617
  tempFileName = CoreUtils::uuidWithoutBraces( QUuid::createUuid() );
153✔
3618
}
306✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc