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

MerginMaps / input / 4795201983

pending completion
4795201983

push

github

Unknown Committer
Unknown Commit Message

7966 of 13315 relevant lines covered (59.83%)

105.28 hits per line

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

77.51
/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
  QObject::connect( this, &MerginApi::pingMerginFinished, this, &MerginApi::getUserInfo, Qt::SingleShotConnection );
22✔
101
  QObject::connect( this, &MerginApi::userInfoReplyFinished, this, &MerginApi::getWorkspaceInfo, Qt::SingleShotConnection );
22✔
102
}
44✔
103

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

110
  mServerType = static_cast<MerginServerType::ServerType>( serverType );
21✔
111

112
  mUserAuth->loadAuthData();
21✔
113
  mUserInfo->loadWorkspacesData();
21✔
114
}
21✔
115

116
MerginUserAuth *MerginApi::userAuth() const
64✔
117
{
118
  return mUserAuth;
64✔
119
}
120

121
MerginUserInfo *MerginApi::userInfo() const
6✔
122
{
123
  return mUserInfo;
6✔
124
}
125

126
MerginWorkspaceInfo *MerginApi::workspaceInfo() const
×
127
{
128
  return mWorkspaceInfo;
×
129
}
130

131
MerginSubscriptionInfo *MerginApi::subscriptionInfo() const
45✔
132
{
133
  return mSubscriptionInfo;
45✔
134
}
135

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

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

146
  QUrlQuery query;
22✔
147

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

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

171
  if ( !searchExpression.isEmpty() )
22✔
172
  {
173
    query.addQueryItem( "name", searchExpression.toUtf8().toPercentEncoding() );
×
174
  }
×
175

176
  query.addQueryItem( "order_params", QStringLiteral( "namespace_asc,name_asc" ) );
22✔
177

178
  // Required query parameters
179
  query.addQueryItem( "page", QString::number( page ) );
22✔
180
  query.addQueryItem( "per_page", QString::number( PROJECT_PER_PAGE ) );
22✔
181

182
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/paginated" ) );
22✔
183
  url.setQuery( query );
22✔
184

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

189
  QString requestId = CoreUtils::uuidWithoutBraces( QUuid::createUuid() );
22✔
190

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

195
  return requestId;
22✔
196
}
22✔
197

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

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

211
  // construct JSON body
212
  QJsonDocument body;
7✔
213
  QJsonObject projects;
7✔
214
  QJsonArray projectsArr = QJsonArray::fromStringList( projectNames );
7✔
215

216
  projects.insert( "projects", projectsArr );
7✔
217
  body.setObject( projects );
7✔
218

219
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/by_names" ) );
7✔
220

221
  QNetworkRequest request = getDefaultRequest( true );
7✔
222
  request.setUrl( url );
7✔
223
  request.setRawHeader( "Content-type", "application/json" );
7✔
224

225
  QString requestId = CoreUtils::uuidWithoutBraces( QUuid::createUuid() );
7✔
226

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

231
  return requestId;
7✔
232
}
7✔
233

234

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

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

247
  DownloadQueueItem item = transaction.downloadQueue.takeFirst();
152✔
248

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

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

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

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

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

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

283
  QString path = getTempProjectDir( getFullProjectName( projectNamespace, projectName ) );
62✔
284
  QDir( path ).removeRecursively();
62✔
285
}
62✔
286

287
QNetworkRequest MerginApi::getDefaultRequest( bool withAuth )
1,029✔
288
{
289
  QNetworkRequest request;
1,029✔
290
  QString info = CoreUtils::appInfo();
1,029✔
291
  request.setRawHeader( "User-Agent", QByteArray( info.toUtf8() ) );
1,029✔
292
  if ( withAuth )
1,029✔
293
    request.setRawHeader( "Authorization", QByteArray( "Bearer " + mUserAuth->authToken() ) );
991✔
294

295
  return request;
1,029✔
296
}
1,029✔
297

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

306
  for ( QString filePath : diff.remoteUpdated )
×
307
  {
308
    if ( CoreUtils::hasProjectFileExtension( filePath ) )
×
309
      return true;
×
310
  }
×
311

312
  return false;
×
313
}
×
314

315
bool MerginApi::supportsSelectiveSync() const
×
316
{
317
  return mSupportsSelectiveSync;
×
318
}
319

320
void MerginApi::setSupportsSelectiveSync( bool supportsSelectiveSync )
4✔
321
{
322
  mSupportsSelectiveSync = supportsSelectiveSync;
4✔
323
}
4✔
324

325
bool MerginApi::apiSupportsSubscriptions() const
18✔
326
{
327
  return mApiSupportsSubscriptions;
18✔
328
}
329

330
void MerginApi::setApiSupportsSubscriptions( bool apiSupportsSubscriptions )
10✔
331
{
332
  if ( mApiSupportsSubscriptions != apiSupportsSubscriptions )
10✔
333
  {
334
    mApiSupportsSubscriptions = apiSupportsSubscriptions;
9✔
335
    emit apiSupportsSubscriptionsChanged();
9✔
336
  }
9✔
337
}
10✔
338

339
#if !defined(USE_MERGIN_DUMMY_API_KEY)
340
#include "merginsecrets.cpp"
341
#endif
342

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

355
void MerginApi::downloadItemReplyFinished()
152✔
356
{
357
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
152✔
358
  Q_ASSERT( r );
152✔
359

360
  QString projectFullName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ) ).toString();
152✔
361
  QString tempFileName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrTempFileName ) ).toString();
152✔
362

363
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
152✔
364
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
152✔
365
  Q_ASSERT( r == transaction.replyPullItem );
152✔
366

367
  if ( r->error() == QNetworkReply::NoError )
152✔
368
  {
369
    QByteArray data = r->readAll();
151✔
370

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

373
    QString tempFolder = getTempProjectDir( projectFullName );
151✔
374
    QString tempFilePath = tempFolder + "/" + tempFileName;
151✔
375
    createPathIfNotExists( tempFilePath );
151✔
376

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

389
    transaction.transferedSize += data.size();
151✔
390
    emit syncProjectStatusChanged( projectFullName, transaction.transferedSize / transaction.totalSize );
151✔
391

392
    transaction.replyPullItem->deleteLater();
151✔
393
    transaction.replyPullItem = nullptr;
151✔
394

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

407
    transaction.replyPullItem->deleteLater();
1✔
408
    transaction.replyPullItem = nullptr;
1✔
409

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

413
    if ( transaction.firstTimeDownload )
1✔
414
    {
415
      Q_ASSERT( !transaction.projectDir.isEmpty() );
1✔
416
      QDir( transaction.projectDir ).removeRecursively();
1✔
417
    }
1✔
418

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

422
    finishProjectSync( projectFullName, false );
1✔
423
  }
1✔
424
}
152✔
425

426
void MerginApi::cacheServerConfig()
39✔
427
{
428
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
39✔
429
  Q_ASSERT( r );
39✔
430

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

433
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
39✔
434
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
39✔
435
  Q_ASSERT( r == transaction.replyPullItem );
39✔
436

437
  if ( r->error() == QNetworkReply::NoError )
39✔
438
  {
439
    QByteArray data = r->readAll();
39✔
440

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

444
    transaction.replyPullItem->deleteLater();
39✔
445
    transaction.replyPullItem = nullptr;
39✔
446

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

458
    transaction.replyPullItem->deleteLater();
×
459
    transaction.replyPullItem = nullptr;
×
460

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

464
    if ( transaction.firstTimeDownload )
×
465
    {
466
      CoreUtils::removeDir( transaction.projectDir );
×
467
    }
×
468

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

472
    finishProjectSync( projectFullName, false );
×
473
  }
×
474
}
39✔
475

476

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

484
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
152✔
485
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
152✔
486

487
  QString chunkID = file.chunks.at( chunkNo );
152✔
488

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

495
  QFile f( filePath );
152✔
496
  QByteArray data;
152✔
497

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

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

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

514
  CoreUtils::log( "push " + projectFullName, QStringLiteral( "Uploading item: " ) + url.toString() );
152✔
515
}
152✔
516

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

524
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
115✔
525
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
115✔
526

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

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

537
  CoreUtils::log( "push " + projectFullName, QStringLiteral( "Starting push request: " ) + url.toString() );
115✔
538
}
115✔
539

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

547
  if ( !mTransactionalStatus.contains( projectFullName ) )
2✔
548
    return;
×
549

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

552
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
2✔
553

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

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

580
    sendPushCancelRequest( projectFullName, transactionUUID );
×
581
  }
×
582
  else
583
  {
584
    Q_ASSERT( false );  // unexpected state
×
585
  }
586
}
2✔
587

588

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

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

602
void MerginApi::cancelPull( const QString &projectFullName )
2✔
603
{
604
  if ( !mTransactionalStatus.contains( projectFullName ) )
2✔
605
    return;
×
606

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

609
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
2✔
610

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

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

636
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
110✔
637
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
110✔
638

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

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

649
  CoreUtils::log( "push " + projectFullName, QStringLiteral( "Requesting transaction finish: " ) + transactionUUID );
110✔
650
}
110✔
651

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

657
  CoreUtils::log( "pull " + projectFullName, "### Starting ###" );
100✔
658

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

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

670
    emit syncProjectStatusChanged( projectFullName, 0 );
100✔
671

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

680
  return pullHasStarted;
100✔
681
}
100✔
682

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

688
  CoreUtils::log( "push " + projectFullName, "### Starting ###" );
143✔
689

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

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

703
    emit syncProjectStatusChanged( projectFullName, 0 );
143✔
704

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

713
  return pushHasStarted;
143✔
714
}
143✔
715

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

725
  mUserAuth->blockSignals( true );
8✔
726
  mUserAuth->setPassword( password );
8✔
727
  mUserAuth->blockSignals( false );
8✔
728

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

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

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

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

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

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

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

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

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

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

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

822
void MerginApi::getUserInfo()
28✔
823
{
824
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
28✔
825
  {
826
    return;
9✔
827
  }
828

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

839
  QNetworkRequest request = getDefaultRequest();
19✔
840
  QUrl url( urlString );
19✔
841
  request.setUrl( url );
19✔
842

843
  QNetworkReply *reply = mManager.get( request );
19✔
844
  CoreUtils::log( "user info", QStringLiteral( "Requesting user info: " ) + url.toString() );
19✔
845
  connect( reply, &QNetworkReply::finished, this, &MerginApi::getUserInfoFinished );
19✔
846
}
28✔
847

848
void MerginApi::getWorkspaceInfo()
12✔
849
{
850
  if ( mServerType == MerginServerType::OLD )
12✔
851
  {
852
    return;
×
853
  }
854

855
  if ( mUserInfo->activeWorkspaceId() == -1 )
12✔
856
  {
857
    return;
2✔
858
  }
859

860
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
10✔
861
  {
862
    return;
×
863
  }
864

865
  QString urlString = mApiRoot + QStringLiteral( "v1/workspace/%1" ).arg( mUserInfo->activeWorkspaceId() );
10✔
866
  QNetworkRequest request = getDefaultRequest();
10✔
867
  QUrl url( urlString );
10✔
868
  request.setUrl( url );
10✔
869

870
  QNetworkReply *reply = mManager.get( request );
10✔
871
  CoreUtils::log( "workspace info", QStringLiteral( "Requesting workspace info: " ) + url.toString() );
10✔
872
  connect( reply, &QNetworkReply::finished, this, &MerginApi::getWorkspaceInfoReplyFinished );
10✔
873
}
12✔
874

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

882
  QString urlString;
11✔
883

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

897
  QNetworkRequest request = getDefaultRequest( true );
11✔
898
  QUrl url( urlString );
11✔
899
  request.setUrl( url );
11✔
900

901
  QNetworkReply *reply = mManager.get( request );
11✔
902

903
  connect( reply, &QNetworkReply::finished, this, &MerginApi::getServiceInfoReplyFinished );
11✔
904

905
  CoreUtils::log( "Service info", QStringLiteral( "Requesting service info: " ) + url.toString() );
11✔
906
}
11✔
907

908
void MerginApi::getServiceInfoReplyFinished()
10✔
909
{
910
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
10✔
911
  Q_ASSERT( r );
10✔
912

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

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

930
    mSubscriptionInfo->clear();
×
931

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

947
  r->deleteLater();
10✔
948
}
10✔
949

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

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

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

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

985
  if ( mApiVersionStatus != MerginApiStatus::OK )
40✔
986
  {
987
    return false;
×
988
  }
989

990
  QString projectFullName = getFullProjectName( projectNamespace, projectName );
40✔
991

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

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

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

1010
  return true;
40✔
1011
}
40✔
1012

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

1020
  QString projectFullName = getFullProjectName( projectNamespace, projectName );
43✔
1021

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

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

1038
  mUserAuth->saveAuthData();
11✔
1039
  mUserInfo->clear();
11✔
1040
}
11✔
1041

1042
void MerginApi::createProjectFinished()
40✔
1043
{
1044
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
40✔
1045
  Q_ASSERT( r );
40✔
1046

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

1049
  QString projectNamespace, projectName;
40✔
1050
  extractProjectName( projectFullName, projectNamespace, projectName );
40✔
1051

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

1057

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

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

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

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

1088
void MerginApi::deleteProjectFinished( bool informUser )
43✔
1089
{
1090
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
43✔
1091
  Q_ASSERT( r );
43✔
1092

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

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

1099
    if ( informUser )
2✔
1100
      emit notify( QStringLiteral( "Project deleted" ) );
2✔
1101

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

1114
void MerginApi::authorizeFinished()
8✔
1115
{
1116
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
8✔
1117
  Q_ASSERT( r );
8✔
1118

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

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

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

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

1181
    if ( !username.isEmpty() && !password.isEmpty() ) // log in immediately
3✔
1182
      authorize( username, password );
3✔
1183

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

1212
void MerginApi::pingMerginReplyFinished()
10✔
1213
{
1214
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
10✔
1215
  Q_ASSERT( r );
10✔
1216
  QString apiVersion;
10✔
1217
  QString serverMsg;
10✔
1218
  bool serverSupportsSubscriptions = false;
10✔
1219

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

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

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

1263
  if ( mApiVersionStatus != MerginApiStatus::OK )
243✔
1264
  {
1265
    return nullptr;
×
1266
  }
1267

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

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

1281
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/%1" ).arg( projectFullName ) );
243✔
1282
  url.setQuery( query );
243✔
1283

1284
  QNetworkRequest request = getDefaultRequest( withAuth );
243✔
1285
  request.setUrl( url );
243✔
1286
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
243✔
1287

1288
  return mManager.get( request );
243✔
1289
}
243✔
1290

1291
bool MerginApi::validateAuth()
779✔
1292
{
1293
  if ( !mUserAuth->hasAuthData() )
779✔
1294
  {
1295
    emit authRequested();
9✔
1296
    return false;
9✔
1297
  }
1298

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

1308
void MerginApi::checkMerginVersion( QString apiVersion, bool serverSupportsSubscriptions, QString msg )
10✔
1309
{
1310
  setApiSupportsSubscriptions( serverSupportsSubscriptions );
10✔
1311

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

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

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

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

1400
  return serverMsg;
50✔
1401
}
50✔
1402

1403

1404
LocalProject MerginApi::getLocalProject( const QString &projectFullName )
7✔
1405
{
1406
  return mLocalProjects.projectFromMerginName( projectFullName );
7✔
1407
}
1408

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

1414
  MerginConfig config = MerginConfig::fromFile( projectDir + "/" + sMerginConfigFile );
41✔
1415

1416
  return compareProjectFiles( projectMetadata.files, projectMetadata.files, localFiles, projectDir, config.isValid, config );
41✔
1417
}
41✔
1418

1419
QString MerginApi::getTempProjectDir( const QString &projectFullName )
337✔
1420
{
1421
  return mDataDir + "/" + TEMP_FOLDER + projectFullName;
337✔
1422
}
×
1423

1424
QString MerginApi::getFullProjectName( QString projectNamespace, QString projectName ) // TODO: move to inpututils?
43,953✔
1425
{
1426
  return QString( "%1/%2" ).arg( projectNamespace ).arg( projectName );
43,953✔
1427
}
×
1428

1429
MerginApiStatus::VersionStatus MerginApi::apiVersionStatus() const
18✔
1430
{
1431
  return mApiVersionStatus;
18✔
1432
}
1433

1434
void MerginApi::setApiVersionStatus( const MerginApiStatus::VersionStatus &apiVersionStatus )
33✔
1435
{
1436
  if ( mApiVersionStatus != apiVersionStatus )
33✔
1437
  {
1438
    mApiVersionStatus = apiVersionStatus;
31✔
1439
    emit apiVersionStatusChanged();
31✔
1440
  }
31✔
1441
}
33✔
1442

1443
void MerginApi::pingMergin()
23✔
1444
{
1445
  if ( mApiVersionStatus == MerginApiStatus::OK ) return;
23✔
1446

1447
  setApiVersionStatus( MerginApiStatus::PENDING );
23✔
1448

1449
  QNetworkRequest request = getDefaultRequest( false );
23✔
1450
  QUrl url( mApiRoot + QStringLiteral( "/ping" ) );
23✔
1451
  request.setUrl( url );
23✔
1452

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

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

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

1477
  if ( projectInfo.isValid() )
1✔
1478
  {
1479
    CoreUtils::removeDir( projectInfo.projectDir + "/.mergin" );
1✔
1480
  }
1✔
1481

1482
  // Update localProject
1483
  mLocalProjects.updateNamespace( projectInfo.projectDir, "" );
1✔
1484
  mLocalProjects.updateLocalVersion( projectInfo.projectDir, -1 );
1✔
1485

1486
  if ( informUser )
1✔
1487
    emit notify( tr( "Project detached from Mergin" ) );
1✔
1488

1489
  emit projectDetached( projectFullName );
1✔
1490
}
1✔
1491

1492
QString MerginApi::apiRoot() const
101✔
1493
{
1494
  return mApiRoot;
101✔
1495
}
1496

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

1509
  if ( newApiRoot != mApiRoot )
4✔
1510
  {
1511
    mApiRoot = newApiRoot;
2✔
1512

1513
    QSettings settings;
2✔
1514
    settings.setValue( QStringLiteral( "Input/apiRoot" ), mApiRoot );
2✔
1515

1516
    emit apiRootChanged();
2✔
1517
  }
2✔
1518
}
4✔
1519

1520
QString MerginApi::merginUserName() const
27✔
1521
{
1522
  return userAuth()->username();
27✔
1523
}
1524

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

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

1545
void MerginApi::listProjectsReplyFinished( QString requestId )
22✔
1546
{
1547
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
22✔
1548
  Q_ASSERT( r );
22✔
1549

1550
  int projectCount = -1;
22✔
1551
  int requestedPage = 1;
22✔
1552
  MerginProjectsList projectList;
22✔
1553

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

1559
    QByteArray data = r->readAll();
22✔
1560
    QJsonDocument doc = QJsonDocument::fromJson( data );
22✔
1561

1562
    if ( doc.isObject() )
22✔
1563
    {
1564
      projectCount = doc.object().value( "count" ).toInt();
22✔
1565
      projectList = parseProjectsFromJson( doc );
22✔
1566
    }
22✔
1567

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

1577
    emit listProjectsFailed();
×
1578
  }
×
1579

1580
  r->deleteLater();
22✔
1581

1582
  emit listProjectsFinished( projectList, projectCount, requestedPage, requestId );
22✔
1583
}
22✔
1584

1585
void MerginApi::listProjectsByNameReplyFinished( QString requestId )
7✔
1586
{
1587
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
7✔
1588
  Q_ASSERT( r );
7✔
1589

1590
  MerginProjectsList projectList;
7✔
1591

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

1606
    emit listProjectsFailed();
×
1607
  }
×
1608

1609
  r->deleteLater();
7✔
1610

1611
  emit listProjectsByNameFinished( projectList, requestId );
7✔
1612
}
7✔
1613

1614

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

1619
  QString dest = projectDir + "/" + filePath;
202✔
1620
  createPathIfNotExists( dest );
202✔
1621

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

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

1641
  f.close();
202✔
1642

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

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

1660

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

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

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

1673
  LocalProject info = mLocalProjects.projectFromMerginName( projectFullName );
9✔
1674

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

1678
  createPathIfNotExists( src );
9✔
1679
  createPathIfNotExists( dest );
9✔
1680
  createPathIfNotExists( basefile );
9✔
1681

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

1686
  //
1687
  // let's first assemble server's file from our basefile + diffs
1688
  //
1689

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

1694
    // TODO: this is a critical failure - we should abort pull
1695
  }
×
1696

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

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

1709
  //
1710
  // now we are ready for the update of our local file
1711
  //
1712
  bool hasConflicts = false;
9✔
1713

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

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

1742
  //
1743
  // finally update our basefile
1744
  //
1745

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

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

1756
    // TODO: this is a critical failure - we should abort pull
1757
  }
×
1758
  return hasConflicts;
9✔
1759
}
9✔
1760

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

1766
  QString projectDir = transaction.projectDir;
123✔
1767
  QString tempProjectDir = getTempProjectDir( projectFullName );
123✔
1768

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

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

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

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

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

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

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

1832
  QDir( tempProjectDir ).removeRecursively();
123✔
1833

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

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

1844
    mLocalProjects.addMerginProject( projectDir, projectNamespace, projectName );
61✔
1845
  }
61✔
1846

1847
  finishProjectSync( projectFullName, true );
123✔
1848
}
123✔
1849

1850

1851
void MerginApi::pushStartReplyFinished()
115✔
1852
{
1853
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
115✔
1854
  Q_ASSERT( r );
115✔
1855

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

1858
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
115✔
1859
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
115✔
1860
  Q_ASSERT( r == transaction.replyPushStart );
115✔
1861

1862
  if ( r->error() == QNetworkReply::NoError )
115✔
1863
  {
1864
    QByteArray data = r->readAll();
115✔
1865

1866
    transaction.replyPushStart->deleteLater();
115✔
1867
    transaction.replyPushStart = nullptr;
115✔
1868

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

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

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

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

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

1900
      transaction.projectMetadata = data;
4✔
1901
      transaction.version = MerginProjectMetadata::fromJson( data ).version;
4✔
1902

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

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

1916
    transaction.replyPushStart->deleteLater();
×
1917
    transaction.replyPushStart = nullptr;
×
1918

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

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

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

1945
    finishProjectSync( projectFullName, false );
×
1946
  }
×
1947
}
115✔
1948

1949
void MerginApi::pushFileReplyFinished()
152✔
1950
{
1951
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
152✔
1952
  Q_ASSERT( r );
152✔
1953

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

1956
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
152✔
1957
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
152✔
1958
  Q_ASSERT( r == transaction.replyPushFile );
152✔
1959

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

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

1969
    transaction.replyPushFile->deleteLater();
151✔
1970
    transaction.replyPushFile = nullptr;
151✔
1971

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

1982
      emit syncProjectStatusChanged( projectFullName, transaction.transferedSize / transaction.totalSize );
149✔
1983
      transaction.pushQueue.removeFirst();
149✔
1984

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

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

2004
    transaction.replyPushFile->deleteLater();
1✔
2005
    transaction.replyPushFile = nullptr;
1✔
2006

2007
    finishProjectSync( projectFullName, false );
1✔
2008
  }
1✔
2009
}
152✔
2010

2011
void MerginApi::pullInfoReplyFinished()
100✔
2012
{
2013
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
100✔
2014
  Q_ASSERT( r );
100✔
2015

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

2018
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
100✔
2019
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
100✔
2020
  Q_ASSERT( r == transaction.replyPullProjectInfo );
100✔
2021

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

2027
    transaction.replyPullProjectInfo->deleteLater();
99✔
2028
    transaction.replyPullProjectInfo = nullptr;
99✔
2029

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

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

2041
    transaction.replyPullProjectInfo->deleteLater();
1✔
2042
    transaction.replyPullProjectInfo = nullptr;
1✔
2043

2044
    finishProjectSync( projectFullName, false );
1✔
2045
  }
1✔
2046
}
100✔
2047

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

2053
  MerginProjectMetadata serverProject = MerginProjectMetadata::fromJson( data );
125✔
2054

2055
  transaction.projectMetadata = data;
125✔
2056
  transaction.version = serverProject.version;
125✔
2057

2058
  LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName );
125✔
2059
  if ( projectInfo.isValid() )
125✔
2060
  {
2061
    transaction.projectDir = projectInfo.projectDir;
63✔
2062

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

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

2078
    // remove any leftover temp files that could be created from previous unsuccessful download
2079
    removeProjectsTempFolder( projectNamespace, projectName );
62✔
2080

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

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

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

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

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

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

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

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

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

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

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

2138
  for ( QString filePath : transaction.diff.remoteUpdated )
138✔
2139
  {
2140
    MerginFile file = serverProject.fileInfo( filePath );
14✔
2141

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

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

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

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

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

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

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

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

2208
  emit pullFilesStarted();
124✔
2209
  downloadNextItem( projectFullName );
124✔
2210
}
124✔
2211

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

2217
  MerginProjectMetadata newServerVersion = MerginProjectMetadata::fromJson( transaction.projectMetadata );
151✔
2218

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

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

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

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

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

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

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

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

2303
    // if it would be possible to add mergin-config locally, it needs to be checked here
2304
  }
2305

2306
  startProjectPull( projectFullName );
112✔
2307
}
151✔
2308

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

2314
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/raw/" ) + projectFullName );
39✔
2315
  QUrlQuery query;
39✔
2316

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

2321
  QNetworkRequest request = getDefaultRequest();
39✔
2322
  request.setUrl( url );
39✔
2323
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
39✔
2324

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

2329
  CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Requesting mergin config: " ) + url.toString() );
39✔
2330
}
39✔
2331

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

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

2356

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

2368

2369
void MerginApi::pushInfoReplyFinished()
143✔
2370
{
2371
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
143✔
2372
  Q_ASSERT( r );
143✔
2373

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

2376
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
143✔
2377
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
143✔
2378
  Q_ASSERT( r == transaction.replyPushProjectInfo );
143✔
2379

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

2386
    transaction.replyPushProjectInfo->deleteLater();
142✔
2387
    transaction.replyPushProjectInfo = nullptr;
142✔
2388

2389
    LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName );
142✔
2390
    transaction.projectDir = projectInfo.projectDir;
142✔
2391
    Q_ASSERT( !transaction.projectDir.isEmpty() );
142✔
2392

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

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

2407
    QList<MerginFile> localFiles = getLocalProjectFiles( transaction.projectDir + "/" );
116✔
2408
    MerginProjectMetadata oldServerProject = MerginProjectMetadata::fromCachedJson( transaction.projectDir + "/" + sMetadataFile );
116✔
2409

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

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

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

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

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

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

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

2452
        if ( geodiffRes == GEODIFF_SUCCESS )
12✔
2453
        {
2454
          QByteArray checksumDiff = getChecksum( diffPath );
12✔
2455

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

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

2466
          diffFiles.append( merginFile );
12✔
2467

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

2477
      updatedMerginFiles.append( merginFile );
19✔
2478
    }
19✔
2479

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

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

2492
      finishProjectSync( projectFullName, true );
1✔
2493
      return;
1✔
2494
    }
2495

2496
    QJsonArray added = prepareUploadChangesJSON( addedMerginFiles );
115✔
2497
    filesToUpload.append( addedMerginFiles );
115✔
2498

2499
    QJsonArray modified = prepareUploadChangesJSON( updatedMerginFiles );
115✔
2500
    filesToUpload.append( updatedMerginFiles );
115✔
2501

2502
    QJsonArray removed = prepareUploadChangesJSON( deletedMerginFiles );
115✔
2503
    // removed not in filesToUpload
2504

2505
    QJsonObject changes;
115✔
2506
    changes.insert( "added", added );
115✔
2507
    changes.insert( "removed", removed );
115✔
2508
    changes.insert( "updated", modified );
115✔
2509
    changes.insert( "renamed", QJsonArray() );
115✔
2510

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

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

2523
    transaction.totalSize = totalSize;
115✔
2524
    transaction.pushQueue = filesToUpload;
115✔
2525
    transaction.pushDiffFiles = diffFiles;
115✔
2526

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

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

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

2544
    transaction.replyPushProjectInfo->deleteLater();
1✔
2545
    transaction.replyPushProjectInfo = nullptr;
1✔
2546

2547
    finishProjectSync( projectFullName, false );
1✔
2548
  }
1✔
2549
}
143✔
2550

2551
void MerginApi::pushFinishReplyFinished()
110✔
2552
{
2553
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
110✔
2554
  Q_ASSERT( r );
110✔
2555

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

2558
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
110✔
2559
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
110✔
2560
  Q_ASSERT( r == transaction.replyPushFinish );
110✔
2561

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

2568
    transaction.replyPushFinish->deleteLater();
110✔
2569
    transaction.replyPushFinish = nullptr;
110✔
2570

2571
    transaction.projectMetadata = data;
110✔
2572
    transaction.version = MerginProjectMetadata::fromJson( data ).version;
110✔
2573

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

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

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

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

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

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

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

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

2633
    transaction.replyPushFinish->deleteLater();
×
2634
    transaction.replyPushFinish = nullptr;
×
2635

2636
    finishProjectSync( projectFullName, false );
×
2637
  }
×
2638
}
110✔
2639

2640
void MerginApi::pushCancelReplyFinished()
1✔
2641
{
2642
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
1✔
2643
  Q_ASSERT( r );
1✔
2644

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

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

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

2660
  r->deleteLater();
1✔
2661
}
1✔
2662

2663
void MerginApi::getUserInfoFinished()
19✔
2664
{
2665
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
19✔
2666
  Q_ASSERT( r );
19✔
2667

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

2691
  emit userInfoReplyFinished();
19✔
2692

2693
  r->deleteLater();
19✔
2694
}
19✔
2695

2696
void MerginApi::getWorkspaceInfoReplyFinished()
10✔
2697
{
2698
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
10✔
2699
  Q_ASSERT( r );
10✔
2700

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

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

2722
  r->deleteLater();
10✔
2723
}
10✔
2724

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

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

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

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

2842
    if ( hasOldServer )
993✔
2843
      oldServerFilesMap.remove( filePath );
815✔
2844
    if ( hasNewServer )
993✔
2845
      newServerFilesMap.remove( filePath );
812✔
2846
  }
993✔
2847

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

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

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

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

2899
    if ( hasOldServer )
179✔
2900
      oldServerFilesMap.remove( file.path );
5✔
2901
  }
388✔
2902

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

2909
  return diff;
281✔
2910
}
281✔
2911

2912
MerginProject MerginApi::parseProjectMetadata( const QJsonObject &proj )
286✔
2913
{
2914
  MerginProject project;
286✔
2915

2916
  if ( proj.isEmpty() )
286✔
2917
  {
2918
    return project;
×
2919
  }
2920

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

2928
  project.projectName = proj.value( QStringLiteral( "name" ) ).toString();
286✔
2929
  project.projectNamespace = proj.value( QStringLiteral( "namespace" ) ).toString();
286✔
2930

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

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

2954

2955
MerginProjectsList MerginApi::parseProjectsFromJson( const QJsonDocument &doc )
29✔
2956
{
2957
  if ( !doc.isObject() )
29✔
2958
    return MerginProjectsList();
×
2959

2960
  QJsonObject object = doc.object();
29✔
2961
  MerginProjectsList result;
29✔
2962

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

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

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

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

3005
QStringList MerginApi::generateChunkIdsForSize( qint64 fileSize )
163✔
3006
{
3007
  qreal rawNoOfChunks = qreal( fileSize ) / UPLOAD_CHUNK_SIZE;
163✔
3008
  int noOfChunks = qCeil( rawNoOfChunks );
163✔
3009

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

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

3024
QJsonArray MerginApi::prepareUploadChangesJSON( const QList<MerginFile> &files )
345✔
3025
{
3026
  QJsonArray jsonArray;
345✔
3027

3028
  for ( MerginFile file : files )
500✔
3029
  {
3030
    QJsonObject fileObject;
155✔
3031
    fileObject.insert( "path", file.path );
155✔
3032

3033
    fileObject.insert( "size", file.size );
155✔
3034
    fileObject.insert( "mtime", file.mtime.toString( Qt::ISODateWithMs ) );
155✔
3035

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

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

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

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

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

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

3075
    // update info of local projects
3076
    mLocalProjects.updateLocalVersion( transaction.projectDir, transaction.version );
238✔
3077

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

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

3091
  if ( transaction.gpkgSchemaChanged )
243✔
3092
  {
3093
    emit projectReloadNeededAfterSync( projectFullName );
96✔
3094
  }
96✔
3095

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

3108
    if ( syncSuccessful )
217✔
3109
    {
3110
      emit projectDataChanged( projectFullName );
212✔
3111
    }
212✔
3112
  }
3113
}
243✔
3114

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

3124
  file.write( data );
238✔
3125
  file.close();
238✔
3126

3127
  return true;
238✔
3128
}
238✔
3129

3130

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

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

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

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

3158
    bool isExcludedFormat = sIgnoreImageExtensions.contains( info.suffix().toLower() );
247✔
3159

3160
    if ( !isExcludedFormat )
247✔
3161
      return false;
20✔
3162

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

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

3191
  return QByteArray();
×
3192
}
1,009✔
3193

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

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

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

3224
void MerginApi::deleteAccountFinished()
1✔
3225
{
3226
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
1✔
3227
  Q_ASSERT( r );
1✔
3228

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

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

3240
    clearAuth();
1✔
3241

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

3258
    emit accountDeleted( false );
×
3259
  }
×
3260

3261
  r->deleteLater();
1✔
3262
}
1✔
3263

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

3271
  QNetworkReply *reply = mManager.get( request );
24✔
3272

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

3277
void MerginApi::getServerConfigReplyFinished()
11✔
3278
{
3279
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
11✔
3280
  Q_ASSERT( r );
11✔
3281

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

3319
  r->deleteLater();
11✔
3320
}
11✔
3321

3322
MerginServerType::ServerType MerginApi::serverType() const
6✔
3323
{
3324
  return mServerType;
6✔
3325
}
3326

3327
void MerginApi::setServerType( const MerginServerType::ServerType &serverType )
16✔
3328
{
3329
  if ( mServerType != serverType )
16✔
3330
  {
3331
    if ( mServerType == MerginServerType::OLD && serverType == MerginServerType::SAAS )
6✔
3332
    {
3333
      emit serverWasUpgraded();
2✔
3334
    }
2✔
3335

3336
    mServerType = serverType;
6✔
3337
    QSettings settings;
6✔
3338
    settings.beginGroup( QStringLiteral( "Input/" ) );
6✔
3339
    settings.setValue( QStringLiteral( "serverType" ), mServerType );
6✔
3340
    settings.endGroup();
6✔
3341
    emit serverTypeChanged();
6✔
3342
    emit apiSupportsWorkspacesChanged();
6✔
3343
  }
6✔
3344
}
16✔
3345

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

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

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

3363
void MerginApi::listWorkspacesReplyFinished()
×
3364
{
3365
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
×
3366
  Q_ASSERT( r );
×
3367

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

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

3399
  r->deleteLater();
×
3400
}
×
3401

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

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

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

3419
void MerginApi::listInvitationsReplyFinished()
×
3420
{
3421
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
×
3422
  Q_ASSERT( r );
×
3423

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

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

3454
  r->deleteLater();
×
3455
}
×
3456

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

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

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

3482
void MerginApi::processInvitationReplyFinished()
×
3483
{
3484
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
×
3485
  Q_ASSERT( r );
×
3486

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

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

3502
  emit processInvitationFinished( accept );
×
3503

3504
  r->deleteLater();
×
3505
}
×
3506

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

3515
  if ( mApiVersionStatus != MerginApiStatus::OK )
2✔
3516
  {
3517
    return false;
×
3518
  }
3519

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

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

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

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

3543
  return true;
2✔
3544
}
2✔
3545

3546
void MerginApi::signOut()
×
3547
{
3548
  clearAuth();
×
3549
}
×
3550

3551
void MerginApi::refreshUserData()
×
3552
{
3553
  getUserInfo();
×
3554

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

3566
void MerginApi::createWorkspaceReplyFinished()
2✔
3567
{
3568
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
2✔
3569
  Q_ASSERT( r );
2✔
3570

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

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

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

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

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

3611
DownloadQueueItem::DownloadQueueItem( const QString &fp, int s, int v, int rf, int rt, bool diff )
459✔
3612
  : filePath( fp ), size( s ), version( v ), rangeFrom( rf ), rangeTo( rt ), downloadDiff( diff )
153✔
3613
{
153✔
3614
  tempFileName = CoreUtils::uuidWithoutBraces( QUuid::createUuid() );
153✔
3615
}
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