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

MerginMaps / input / 6689926879

30 Oct 2023 08:03AM UTC coverage: 62.255%. Remained the same
6689926879

push

github

wonder-sk
Download data from MM server in parallel

Using up to 5 requests at once...

7691 of 12354 relevant lines covered (62.26%)

105.32 hits per line

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

79.44
/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
#include <QElapsedTimer>
22

23
#include "projectchecksumcache.h"
24
#include "coreutils.h"
25
#include "geodiffutils.h"
26
#include "localprojectsmanager.h"
27
#include "../app/enumhelper.h"
28

29
#include <geodiff.h>
30

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

41

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

63
  qRegisterMetaType<Transactions>();
23✔
64

65
  QObject::connect( this, &MerginApi::authChanged, this, &MerginApi::saveAuthData );
23✔
66
  QObject::connect( this, &MerginApi::apiRootChanged, this, &MerginApi::pingMergin );
23✔
67
  QObject::connect( this, &MerginApi::apiRootChanged, this, &MerginApi::getServerConfig );
23✔
68
  QObject::connect( this, &MerginApi::pingMerginFinished, this, &MerginApi::checkMerginVersion );
23✔
69
  QObject::connect( this, &MerginApi::workspaceCreated, this, &MerginApi::getUserInfo );
23✔
70
  QObject::connect( this, &MerginApi::serverTypeChanged, this, [this]()
23✔
71
  {
72
    if ( mUserAuth->hasAuthData() )
6✔
73
    {
74
      // do not call /user/profile when user just logged out
75
      getUserInfo();
×
76
    }
77
  } );
6✔
78
  QObject::connect( this, &MerginApi::processInvitationFinished, this, &MerginApi::getUserInfo );
23✔
79
  QObject::connect( this, &MerginApi::getWorkspaceInfoFinished, this, &MerginApi::getServiceInfo );
23✔
80
  QObject::connect( mUserInfo, &MerginUserInfo::userInfoChanged, this, &MerginApi::userInfoChanged );
23✔
81
  QObject::connect( mUserInfo, &MerginUserInfo::activeWorkspaceChanged, this, &MerginApi::activeWorkspaceChanged );
23✔
82
  QObject::connect( mUserInfo, &MerginUserInfo::activeWorkspaceChanged, this, &MerginApi::getWorkspaceInfo );
23✔
83
  QObject::connect( mUserInfo, &MerginUserInfo::hasWorkspacesChanged, this, &MerginApi::hasWorkspacesChanged );
23✔
84
  QObject::connect( mSubscriptionInfo, &MerginSubscriptionInfo::subscriptionInfoChanged, this, &MerginApi::subscriptionInfoChanged );
23✔
85
  QObject::connect( mSubscriptionInfo, &MerginSubscriptionInfo::planProductIdChanged, this, &MerginApi::onPlanProductIdChanged );
23✔
86
  QObject::connect( mUserAuth, &MerginUserAuth::authChanged, this, &MerginApi::authChanged );
23✔
87
  QObject::connect( mUserAuth, &MerginUserAuth::authChanged, this, [this]()
23✔
88
  {
89
    if ( mUserAuth->hasAuthData() )
12✔
90
    {
91
      // do not call /user/profile when user just logged out
92
      getUserInfo();
9✔
93
    }
94
  } );
12✔
95

96
  //
97
  // check if the cache is up to date:
98
  //  - server url and type
99
  //  - user auth and info
100
  //  - workspace info
101
  //
102

103
  getServerConfig();
23✔
104
  pingMergin();
23✔
105

106
  if ( mUserAuth->hasAuthData() )
23✔
107
  {
108
    QObject::connect( this, &MerginApi::pingMerginFinished, this, &MerginApi::getUserInfo, Qt::SingleShotConnection );
18✔
109
    QObject::connect( this, &MerginApi::userInfoReplyFinished, this, &MerginApi::getWorkspaceInfo, Qt::SingleShotConnection );
18✔
110
  }
111
}
23✔
112

113
void MerginApi::loadCache()
22✔
114
{
115
  QSettings settings;
22✔
116
  mApiRoot = settings.value( QStringLiteral( "Input/apiRoot" ) ).toString();
22✔
117
  int serverType = settings.value( QStringLiteral( "Input/serverType" ) ).toInt();
22✔
118

119
  mServerType = static_cast<MerginServerType::ServerType>( serverType );
22✔
120

121
  mUserAuth->loadAuthData();
22✔
122
  mUserInfo->loadWorkspacesData();
22✔
123
}
22✔
124

125
MerginUserAuth *MerginApi::userAuth() const
70✔
126
{
127
  return mUserAuth;
70✔
128
}
129

130
MerginUserInfo *MerginApi::userInfo() const
16✔
131
{
132
  return mUserInfo;
16✔
133
}
134

135
MerginWorkspaceInfo *MerginApi::workspaceInfo() const
9✔
136
{
137
  return mWorkspaceInfo;
9✔
138
}
139

140
MerginSubscriptionInfo *MerginApi::subscriptionInfo() const
124✔
141
{
142
  return mSubscriptionInfo;
124✔
143
}
144

145
QString MerginApi::listProjects( const QString &searchExpression, const QString &flag, const int page )
22✔
146
{
147
  bool authorize = flag != "public";
22✔
148

149
  if ( ( authorize && !validateAuth() ) || mApiVersionStatus != MerginApiStatus::OK )
22✔
150
  {
151
    emit listProjectsFailed();
×
152
    return QString();
×
153
  }
154

155
  QUrlQuery query;
22✔
156

157
  if ( flag == "workspace" )
22✔
158
  {
159
    if ( mUserInfo->activeWorkspaceId() < 0 )
×
160
    {
161
      emit listProjectsFailed();
×
162
      return QString();
×
163
    }
164

165
    query.addQueryItem( "only_namespace", mUserInfo->activeWorkspaceName() );
×
166
  }
167
  else if ( flag == "created" )
22✔
168
  {
169
    query.addQueryItem( "flag", "created" );
22✔
170
  }
171
  else if ( flag == "shared" )
×
172
  {
173
    query.addQueryItem( "flag", "shared" );
×
174
  }
175
  else if ( flag == "public" )
×
176
  {
177
    query.addQueryItem( "only_public", "true" );
×
178
  }
179

180
  if ( !searchExpression.isEmpty() )
22✔
181
  {
182
    query.addQueryItem( "name", searchExpression.toUtf8().toPercentEncoding() );
×
183
  }
184

185
  query.addQueryItem( "order_params", QStringLiteral( "namespace_asc,name_asc" ) );
22✔
186

187
  // Required query parameters
188
  query.addQueryItem( "page", QString::number( page ) );
22✔
189
  query.addQueryItem( "per_page", QString::number( PROJECT_PER_PAGE ) );
22✔
190

191
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/paginated" ) );
44✔
192
  url.setQuery( query );
22✔
193

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

198
  QString requestId = CoreUtils::uuidWithoutBraces( QUuid::createUuid() );
22✔
199

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

204
  return requestId;
22✔
205
}
22✔
206

207
QString MerginApi::listProjectsByName( const QStringList &projectNames )
7✔
208
{
209
  if ( mApiVersionStatus != MerginApiStatus::OK )
7✔
210
  {
211
    emit listProjectsFailed();
×
212
    return QLatin1String();
×
213
  }
214

215
  const int listProjectsByNameApiLimit = 50;
7✔
216
  QStringList projectNamesToRequest( projectNames );
7✔
217

218
  if ( projectNamesToRequest.count() > listProjectsByNameApiLimit )
7✔
219
  {
220
    CoreUtils::log( "list projects by name", QStringLiteral( "Too many local projects: " ) + QString::number( projectNames.count(), 'f', 0 ) );
×
221
    const int projectsToRemoveCount = projectNames.count() - listProjectsByNameApiLimit;
×
222
    QString msg = tr( "Please remove some projects as the app currently\nonly allows up to %1 downloaded projects." ).arg( listProjectsByNameApiLimit );
×
223
    notify( msg );
×
224
    projectNamesToRequest.erase( projectNamesToRequest.begin() + listProjectsByNameApiLimit, projectNamesToRequest.end() );
×
225
    Q_ASSERT( projectNamesToRequest.count() == listProjectsByNameApiLimit );
×
226
  }
×
227

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

233
  // construct JSON body
234
  QJsonDocument body;
7✔
235
  QJsonObject projects;
7✔
236
  QJsonArray projectsArr = QJsonArray::fromStringList( projectNamesToRequest );
7✔
237

238
  projects.insert( "projects", projectsArr );
7✔
239
  body.setObject( projects );
7✔
240

241
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/by_names" ) );
14✔
242

243
  QNetworkRequest request = getDefaultRequest( true );
7✔
244
  request.setUrl( url );
7✔
245
  request.setRawHeader( "Content-type", "application/json" );
7✔
246

247
  QString requestId = CoreUtils::uuidWithoutBraces( QUuid::createUuid() );
7✔
248

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

253
  return requestId;
7✔
254
}
7✔
255

256

257
void MerginApi::downloadNextItem( const QString &projectFullName )
153✔
258
{
259
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
153✔
260
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
153✔
261

262
  Q_ASSERT( !transaction.downloadQueue.isEmpty() );
153✔
263

264
  DownloadQueueItem item = transaction.downloadQueue.takeFirst();
153✔
265

266
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/raw/" ) + projectFullName );
306✔
267
  QUrlQuery query;
153✔
268
  // Handles special chars in a filePath (e.g prevents to convert "+" sign into a space)
269
  query.addQueryItem( "file", item.filePath.toUtf8().toPercentEncoding() );
153✔
270
  query.addQueryItem( "version", QStringLiteral( "v%1" ).arg( item.version ) );
153✔
271
  if ( item.downloadDiff )
153✔
272
    query.addQueryItem( "diff", "true" );
10✔
273
  url.setQuery( query );
153✔
274

275
  QNetworkRequest request = getDefaultRequest();
153✔
276
  request.setUrl( url );
153✔
277
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
153✔
278
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrTempFileName ), item.tempFileName );
153✔
279

280
  QString range;
153✔
281
  if ( item.rangeFrom != -1 && item.rangeTo != -1 )
153✔
282
  {
283
    range = QStringLiteral( "bytes=%1-%2" ).arg( item.rangeFrom ).arg( item.rangeTo );
143✔
284
    request.setRawHeader( "Range", range.toUtf8() );
143✔
285
  }
286

287
  QNetworkReply *reply = mManager.get( request );
153✔
288
  connect( reply, &QNetworkReply::finished, this, &MerginApi::downloadItemReplyFinished );
153✔
289
  transaction.replyPullItems.insert( reply );
153✔
290

291
  CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Requesting item: " ) + url.toString() +
306✔
292
                  ( !range.isEmpty() ? " Range: " + range : QString() ) );
306✔
293
}
153✔
294

295
void MerginApi::removeProjectsTempFolder( const QString &projectNamespace, const QString &projectName )
62✔
296
{
297
  if ( projectNamespace.isEmpty() || projectName.isEmpty() )
62✔
298
    return; // otherwise we could remove enitre users temp or entire .temp
×
299

300
  QString path = getTempProjectDir( getFullProjectName( projectNamespace, projectName ) );
124✔
301
  QDir( path ).removeRecursively();
62✔
302
}
62✔
303

304
QNetworkRequest MerginApi::getDefaultRequest( bool withAuth )
1,077✔
305
{
306
  QNetworkRequest request;
1,077✔
307
  QString info = CoreUtils::appInfo();
1,077✔
308
  request.setRawHeader( "User-Agent", QByteArray( info.toUtf8() ) );
1,077✔
309
  if ( withAuth )
1,077✔
310
    request.setRawHeader( "Authorization", QByteArray( "Bearer " + mUserAuth->authToken() ) );
1,037✔
311

312
  return request;
2,154✔
313
}
1,077✔
314

315
bool MerginApi::projectFileHasBeenUpdated( const ProjectDiff &diff )
147✔
316
{
317
  for ( QString filePath : diff.remoteAdded )
149✔
318
  {
319
    if ( CoreUtils::hasProjectFileExtension( filePath ) )
2✔
320
      return true;
×
321
  }
2✔
322

323
  for ( QString filePath : diff.remoteUpdated )
153✔
324
  {
325
    if ( CoreUtils::hasProjectFileExtension( filePath ) )
6✔
326
      return true;
×
327
  }
6✔
328

329
  return false;
147✔
330
}
331

332
bool MerginApi::supportsSelectiveSync() const
×
333
{
334
  return mSupportsSelectiveSync;
×
335
}
336

337
void MerginApi::setSupportsSelectiveSync( bool supportsSelectiveSync )
4✔
338
{
339
  mSupportsSelectiveSync = supportsSelectiveSync;
4✔
340
}
4✔
341

342
bool MerginApi::apiSupportsSubscriptions() const
31✔
343
{
344
  return mApiSupportsSubscriptions;
31✔
345
}
346

347
void MerginApi::setApiSupportsSubscriptions( bool apiSupportsSubscriptions )
11✔
348
{
349
  if ( mApiSupportsSubscriptions != apiSupportsSubscriptions )
11✔
350
  {
351
    mApiSupportsSubscriptions = apiSupportsSubscriptions;
10✔
352
    emit apiSupportsSubscriptionsChanged();
10✔
353
  }
354
}
11✔
355

356
#if !defined(USE_MERGIN_DUMMY_API_KEY)
357
#include "merginsecrets.cpp"
358
#endif
359

360
QString MerginApi::getApiKey( const QString &serverName )
10✔
361
{
362
#if defined(USE_MERGIN_DUMMY_API_KEY)
363
  Q_UNUSED( serverName );
364
#else
365
  QString secretKey = __getSecretApiKey( serverName );
10✔
366
  if ( !secretKey.isEmpty() )
10✔
367
    return secretKey;
10✔
368
#endif
369
  return "not-secret-key";
×
370
}
10✔
371

372
void MerginApi::downloadItemReplyFinished()
153✔
373
{
374
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
153✔
375
  Q_ASSERT( r );
153✔
376

377
  QString projectFullName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ) ).toString();
306✔
378
  QString tempFileName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrTempFileName ) ).toString();
306✔
379

380
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
153✔
381
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
153✔
382
  Q_ASSERT( transaction.replyPullItems.contains( r ) );
153✔
383

384
  if ( r->error() == QNetworkReply::NoError )
153✔
385
  {
386
    QByteArray data = r->readAll();
151✔
387

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

390
    QString tempFolder = getTempProjectDir( projectFullName );
151✔
391
    QString tempFilePath = tempFolder + "/" + tempFileName;
151✔
392
    createPathIfNotExists( tempFilePath );
151✔
393

394
    // save to a tmp file, assemble at the end
395
    QFile file( tempFilePath );
151✔
396
    if ( file.open( QIODevice::WriteOnly ) )
151✔
397
    {
398
      file.write( data );
151✔
399
      file.close();
151✔
400
    }
401
    else
402
    {
403
      CoreUtils::log( "pull " + projectFullName, "Failed to open for writing: " + file.fileName() );
×
404
    }
405

406
    transaction.transferedSize += data.size();
151✔
407
    emit syncProjectStatusChanged( projectFullName, transaction.transferedSize / transaction.totalSize );
151✔
408

409
    transaction.replyPullItems.remove( r );
151✔
410
    r->deleteLater();
151✔
411

412
    if ( !transaction.downloadQueue.isEmpty() )
151✔
413
    {
414
      // one request finished, let's start another one
415
      downloadNextItem( projectFullName );
×
416
    }
417
    else if ( transaction.replyPullItems.isEmpty() )
151✔
418
    {
419
      // nothing else to download and all requests are finished, we're done
420
      finalizeProjectPull( projectFullName );
89✔
421
    }
422
    else
423
    {
424
      // no more requests to start, but there are pending requests - let's do nothing and wait
425
    }
426
  }
151✔
427
  else
428
  {
429
    QString serverMsg = extractServerErrorMsg( r->readAll() );
2✔
430
    if ( serverMsg.isEmpty() )
2✔
431
    {
432
      serverMsg = r->errorString();
2✔
433
    }
434
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) );
2✔
435

436
    transaction.replyPullItems.remove( r );
2✔
437
    r->deleteLater();
2✔
438

439
    if ( !transaction.pullItemsAborting )
2✔
440
    {
441
      // the first failed request will abort all the other pending requests too, and finish pull with error
442
      abortPullItems( projectFullName );
×
443

444
      // signal a networking error - we may retry
445
      int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
×
446
      emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: downloadFile" ), httpCode, projectFullName );
×
447
    }
448
    else
449
    {
450
      // do nothing more: we are already aborting requests and handling finalization in abortPullItems()
451
    }
452
  }
2✔
453
}
153✔
454

455
void MerginApi::abortPullItems( const QString &projectFullName )
1✔
456
{
457
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
1✔
458
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
1✔
459

460
  transaction.pullItemsAborting = true;
1✔
461

462
  CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Aborting pending downloads" ) );
1✔
463
  for ( QNetworkReply *r : transaction.replyPullItems )
3✔
464
    r->abort();  // abort will trigger downloadItemReplyFinished slot
2✔
465

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

469
  if ( transaction.firstTimeDownload )
1✔
470
  {
471
    Q_ASSERT( !transaction.projectDir.isEmpty() );
1✔
472
    QDir( transaction.projectDir ).removeRecursively();
1✔
473
  }
474

475
  finishProjectSync( projectFullName, false );
1✔
476
}
1✔
477

478
void MerginApi::cacheServerConfig()
39✔
479
{
480
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
39✔
481
  Q_ASSERT( r );
39✔
482

483
  QString projectFullName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ) ).toString();
78✔
484

485
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
39✔
486
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
39✔
487
  Q_ASSERT( r == transaction.replyPullServerConfig );
39✔
488

489
  if ( r->error() == QNetworkReply::NoError )
39✔
490
  {
491
    QByteArray data = r->readAll();
39✔
492

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

496
    transaction.replyPullServerConfig->deleteLater();
39✔
497
    transaction.replyPullServerConfig = nullptr;
39✔
498

499
    prepareDownloadConfig( projectFullName, true );
39✔
500
  }
39✔
501
  else
502
  {
503
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
504
    if ( serverMsg.isEmpty() )
×
505
    {
506
      serverMsg = r->errorString();
×
507
    }
508
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Failed to cache mergin config - %1. %2" ).arg( r->errorString(), serverMsg ) );
×
509

510
    transaction.replyPullServerConfig->deleteLater();
×
511
    transaction.replyPullServerConfig = nullptr;
×
512

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

516
    if ( transaction.firstTimeDownload )
×
517
    {
518
      CoreUtils::removeDir( transaction.projectDir );
×
519
    }
520

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

524
    finishProjectSync( projectFullName, false );
×
525
  }
×
526
}
39✔
527

528

529
void MerginApi::pushFile( const QString &projectFullName, const QString &transactionUUID, MerginFile file, int chunkNo )
152✔
530
{
531
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
152✔
532
  {
533
    return;
×
534
  }
535

536
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
152✔
537
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
152✔
538

539
  QString chunkID = file.chunks.at( chunkNo );
152✔
540

541
  QString filePath;
152✔
542
  if ( file.diffName.isEmpty() )
152✔
543
    filePath = transaction.projectDir + "/" + file.path;
140✔
544
  else  // use diff file instead of full file
545
    filePath = transaction.projectDir + "/.mergin/" + file.diffName;
12✔
546

547
  QFile f( filePath );
152✔
548
  QByteArray data;
152✔
549

550
  if ( f.open( QIODevice::ReadOnly ) )
152✔
551
  {
552
    f.seek( chunkNo * UPLOAD_CHUNK_SIZE );
152✔
553
    data = f.read( UPLOAD_CHUNK_SIZE );
152✔
554
  }
555

556
  QNetworkRequest request = getDefaultRequest();
152✔
557
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/push/chunk/%1/%2" ).arg( transactionUUID, chunkID ) );
304✔
558
  request.setUrl( url );
152✔
559
  request.setRawHeader( "Content-Type", "application/octet-stream" );
152✔
560
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
152✔
561

562
  Q_ASSERT( !transaction.replyPushFile );
152✔
563
  transaction.replyPushFile = mManager.post( request, data );
152✔
564
  connect( transaction.replyPushFile, &QNetworkReply::finished, this, &MerginApi::pushFileReplyFinished );
152✔
565

566
  CoreUtils::log( "push " + projectFullName, QStringLiteral( "Uploading item: " ) + url.toString() );
304✔
567
}
152✔
568

569
void MerginApi::pushStart( const QString &projectFullName, const QByteArray &json )
115✔
570
{
571
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
115✔
572
  {
573
    return;
×
574
  }
575

576
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
115✔
577
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
115✔
578

579
  QNetworkRequest request = getDefaultRequest();
115✔
580
  QUrl url( mApiRoot + QStringLiteral( "v1/project/push/%1" ).arg( projectFullName ) );
230✔
581
  request.setUrl( url );
115✔
582
  request.setRawHeader( "Content-Type", "application/json" );
115✔
583
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
115✔
584

585
  Q_ASSERT( !transaction.replyPushStart );
115✔
586
  transaction.replyPushStart = mManager.post( request, json );
115✔
587
  connect( transaction.replyPushStart, &QNetworkReply::finished, this, &MerginApi::pushStartReplyFinished );
115✔
588

589
  CoreUtils::log( "push " + projectFullName, QStringLiteral( "Starting push request: " ) + url.toString() );
230✔
590
}
115✔
591

592
void MerginApi::cancelPush( const QString &projectFullName )
2✔
593
{
594
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
2✔
595
  {
596
    return;
×
597
  }
598

599
  if ( !mTransactionalStatus.contains( projectFullName ) )
2✔
600
    return;
×
601

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

604
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
2✔
605

606
  // There is an open transaction, abort it followed by calling cancelUpload again.
607
  if ( transaction.replyPushProjectInfo )
2✔
608
  {
609
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Aborting project info request" ) );
1✔
610
    transaction.replyPushProjectInfo->abort();  // will trigger uploadInfoReplyFinished slot and emit sync finished
1✔
611
  }
612
  else if ( transaction.replyPushStart )
1✔
613
  {
614
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Aborting upload start" ) );
×
615
    transaction.replyPushStart->abort();  // will trigger uploadStartReplyFinished slot and emit sync finished
×
616
  }
617
  else if ( transaction.replyPushFile )
1✔
618
  {
619
    QString transactionUUID = transaction.transactionUUID;  // copy transaction uuid as the transaction object will be gone after abort
1✔
620
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Aborting upload file" ) );
1✔
621
    transaction.replyPushFile->abort();  // will trigger pushFileReplyFinished slot and emit sync finished
1✔
622

623
    // also need to cancel the transaction
624
    sendPushCancelRequest( projectFullName, transactionUUID );
1✔
625
  }
1✔
626
  else if ( transaction.replyPushFinish )
×
627
  {
628
    QString transactionUUID = transaction.transactionUUID;  // copy transaction uuid as the transaction object will be gone after abort
×
629
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Aborting upload finish" ) );
×
630
    transaction.replyPushFinish->abort();  // will trigger pushFinishReplyFinished slot and emit sync finished
×
631

632
    sendPushCancelRequest( projectFullName, transactionUUID );
×
633
  }
×
634
  else
635
  {
636
    Q_ASSERT( false );  // unexpected state
×
637
  }
638
}
639

640

641
void MerginApi::sendPushCancelRequest( const QString &projectFullName, const QString &transactionUUID )
1✔
642
{
643
  QNetworkRequest request = getDefaultRequest();
1✔
644
  QUrl url( mApiRoot + QStringLiteral( "v1/project/push/cancel/%1" ).arg( transactionUUID ) );
2✔
645
  request.setUrl( url );
1✔
646
  request.setRawHeader( "Content-Type", "application/json" );
1✔
647
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
1✔
648

649
  QNetworkReply *reply = mManager.post( request, QByteArray() );
1✔
650
  connect( reply, &QNetworkReply::finished, this, &MerginApi::pushCancelReplyFinished );
1✔
651
  CoreUtils::log( "push " + projectFullName, QStringLiteral( "Requesting upload transaction cancel: " ) + url.toString() );
2✔
652
}
1✔
653

654
void MerginApi::cancelPull( const QString &projectFullName )
2✔
655
{
656
  if ( !mTransactionalStatus.contains( projectFullName ) )
2✔
657
    return;
×
658

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

661
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
2✔
662

663
  if ( transaction.replyPullProjectInfo )
2✔
664
  {
665
    // we're still fetching project info
666
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Aborting project info request" ) );
1✔
667
    transaction.replyPullProjectInfo->abort();  // abort will trigger pullInfoReplyFinished() slot
1✔
668
  }
669
  else if ( transaction.replyPullServerConfig )
1✔
670
  {
671
    // we're getting server config info
672
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Aborting server config download" ) );
×
673
    transaction.replyPullServerConfig->abort();  // abort will trigger cacheServerConfig slot
×
674
  }
675
  else if ( !transaction.replyPullItems.isEmpty() )
1✔
676
  {
677
    // we're already downloading some files
678
    abortPullItems( projectFullName );
1✔
679
  }
680
  else
681
  {
682
    Q_ASSERT( false );  // unexpected state
×
683
  }
684
}
685

686
void MerginApi::pushFinish( const QString &projectFullName, const QString &transactionUUID )
110✔
687
{
688
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
110✔
689
  {
690
    return;
×
691
  }
692

693
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
110✔
694
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
110✔
695

696
  QNetworkRequest request = getDefaultRequest();
110✔
697
  QUrl url( mApiRoot + QStringLiteral( "v1/project/push/finish/%1" ).arg( transactionUUID ) );
220✔
698
  request.setUrl( url );
110✔
699
  request.setRawHeader( "Content-Type", "application/json" );
110✔
700
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
110✔
701

702
  Q_ASSERT( !transaction.replyPushFinish );
110✔
703
  transaction.replyPushFinish = mManager.post( request, QByteArray() );
110✔
704
  connect( transaction.replyPushFinish, &QNetworkReply::finished, this, &MerginApi::pushFinishReplyFinished );
110✔
705

706
  CoreUtils::log( "push " + projectFullName, QStringLiteral( "Requesting transaction finish: " ) + transactionUUID );
110✔
707
}
110✔
708

709
bool MerginApi::pullProject( const QString &projectNamespace, const QString &projectName, bool withAuth )
100✔
710
{
711
  QString projectFullName = getFullProjectName( projectNamespace, projectName );
200✔
712
  bool pullHasStarted = false;
100✔
713

714
  CoreUtils::log( "pull " + projectFullName, "### Starting ###" );
100✔
715

716
  QNetworkReply *reply = getProjectInfo( projectFullName, withAuth );
100✔
717
  if ( reply )
100✔
718
  {
719
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Requesting project info: " ) + reply->request().url().toString() );
200✔
720

721
    Q_ASSERT( !mTransactionalStatus.contains( projectFullName ) );
100✔
722
    mTransactionalStatus.insert( projectFullName, TransactionStatus() );
100✔
723
    mTransactionalStatus[projectFullName].replyPullProjectInfo = reply;
100✔
724
    mTransactionalStatus[projectFullName].configAllowed = mSupportsSelectiveSync;
100✔
725
    mTransactionalStatus[projectFullName].type = TransactionStatus::Pull;
100✔
726

727
    emit syncProjectStatusChanged( projectFullName, 0 );
100✔
728

729
    connect( reply, &QNetworkReply::finished, this, &MerginApi::pullInfoReplyFinished );
100✔
730
    pullHasStarted = true;
100✔
731
  }
732
  else
733
  {
734
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "FAILED to create project info request!" ) );
×
735
  }
736

737
  return pullHasStarted;
100✔
738
}
100✔
739

740
bool MerginApi::pushProject( const QString &projectNamespace, const QString &projectName, bool isInitialPush )
143✔
741
{
742
  QString projectFullName = getFullProjectName( projectNamespace, projectName );
286✔
743
  bool pushHasStarted = false;
143✔
744

745
  CoreUtils::log( "push " + projectFullName, "### Starting ###" );
143✔
746

747
  QNetworkReply *reply = getProjectInfo( projectFullName );
143✔
748
  if ( reply )
143✔
749
  {
750
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Requesting project info: " ) + reply->request().url().toString() );
286✔
751

752
    // create entry about pending upload for the project
753
    Q_ASSERT( !mTransactionalStatus.contains( projectFullName ) );
143✔
754
    mTransactionalStatus.insert( projectFullName, TransactionStatus() );
143✔
755
    mTransactionalStatus[projectFullName].replyPushProjectInfo = reply;
143✔
756
    mTransactionalStatus[projectFullName].isInitialPush = isInitialPush;
143✔
757
    mTransactionalStatus[projectFullName].configAllowed = mSupportsSelectiveSync;
143✔
758
    mTransactionalStatus[projectFullName].type = TransactionStatus::Push;
143✔
759

760
    emit syncProjectStatusChanged( projectFullName, 0 );
143✔
761

762
    connect( reply, &QNetworkReply::finished, this, &MerginApi::pushInfoReplyFinished );
143✔
763
    pushHasStarted = true;
143✔
764
  }
765
  else
766
  {
767
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED to create project info request!" ) );
×
768
  }
769

770
  return pushHasStarted;
143✔
771
}
143✔
772

773
void MerginApi::authorize( const QString &login, const QString &password )
8✔
774
{
775
  if ( login.isEmpty() || password.isEmpty() )
8✔
776
  {
777
    emit authFailed();
×
778
    emit notify( QStringLiteral( "Please enter your login details" ) );
×
779
    return;
×
780
  }
781

782
  mUserAuth->blockSignals( true );
8✔
783
  mUserAuth->setPassword( password );
8✔
784
  mUserAuth->blockSignals( false );
8✔
785

786
  QNetworkRequest request = getDefaultRequest( false );
8✔
787
  QString urlString = mApiRoot + QStringLiteral( "v1/auth/login" );
16✔
788
  QUrl url( urlString );
8✔
789
  request.setUrl( url );
8✔
790
  request.setRawHeader( "Content-Type", "application/json" );
8✔
791

792
  QJsonDocument jsonDoc;
8✔
793
  QJsonObject jsonObject;
8✔
794
  jsonObject.insert( QStringLiteral( "login" ), login );
16✔
795
  jsonObject.insert( QStringLiteral( "password" ), mUserAuth->password() );
16✔
796
  jsonDoc.setObject( jsonObject );
8✔
797
  QByteArray json = jsonDoc.toJson( QJsonDocument::Compact );
8✔
798

799
  QNetworkReply *reply = mManager.post( request, json );
8✔
800
  connect( reply, &QNetworkReply::finished, this, &MerginApi::authorizeFinished );
8✔
801
  CoreUtils::log( "auth", QStringLiteral( "Requesting authorization: " ) + url.toString() );
16✔
802
}
8✔
803

804
void MerginApi::registerUser( const QString &username,
8✔
805
                              const QString &email,
806
                              const QString &password,
807
                              const QString &confirmPassword,
808
                              bool acceptedTOC )
809
{
810
  // Some very basic checks, so we do not validate everything
811
  if ( username.isEmpty() || username.length() < 4 )
8✔
812
  {
813
    QString msg = tr( "Username must have at least 4 characters" );
1✔
814
    emit registrationFailed( msg, RegistrationError::RegistrationErrorType::USERNAME );
1✔
815
    return;
1✔
816
  }
1✔
817

818
  if ( !CoreUtils::isValidName( username ) )
7✔
819
  {
820
    QString msg = tr( "Username contains invalid characters" );
×
821
    emit registrationFailed( msg, RegistrationError::RegistrationErrorType::USERNAME );
×
822
    return;
×
823
  }
×
824

825
  if ( email.isEmpty() || !email.contains( '@' ) || !email.contains( '.' ) )
7✔
826
  {
827
    QString msg = tr( "Please enter a valid email" );
1✔
828
    emit registrationFailed( msg, RegistrationError::RegistrationErrorType::EMAIL );
1✔
829
    return;
1✔
830
  }
1✔
831

832
  if ( password.isEmpty() || password.length() < 8 )
6✔
833
  {
834
    QString msg = tr( "Password not strong enough. It must"
1✔
835
                      "%1 be at least 8 characters long"
836
                      "%1 contain lowercase characters"
837
                      "%1 contain uppercase characters"
838
                      "%1 contain digits or special characters" )
839
                  .arg( "<br />  -" );
2✔
840
    emit registrationFailed( msg, RegistrationError::RegistrationErrorType::PASSWORD );
1✔
841
    return;
1✔
842

843
  }
1✔
844

845
  if ( confirmPassword != password )
5✔
846
  {
847
    QString msg = tr( "Passwords do not match" );
1✔
848
    emit registrationFailed( msg, RegistrationError::RegistrationErrorType::CONFIRM_PASSWORD );
1✔
849
    return;
1✔
850
  }
1✔
851

852
  if ( !acceptedTOC )
4✔
853
  {
854
    QString msg = tr( "Please accept Terms and Privacy Policy" );
1✔
855
    emit registrationFailed( msg, RegistrationError::RegistrationErrorType::TOC );
1✔
856
    return;
1✔
857
  }
1✔
858

859
  // request
860
  QNetworkRequest request = getDefaultRequest( false );
3✔
861
  QString urlString = mApiRoot + QStringLiteral( "v1/auth/register" );
6✔
862
  QUrl url( urlString );
3✔
863
  request.setUrl( url );
3✔
864
  request.setRawHeader( "Content-Type", "application/json" );
3✔
865

866
  QJsonDocument jsonDoc;
3✔
867
  QJsonObject jsonObject;
3✔
868
  jsonObject.insert( QStringLiteral( "username" ), username );
6✔
869
  jsonObject.insert( QStringLiteral( "email" ), email );
6✔
870
  jsonObject.insert( QStringLiteral( "password" ), password );
6✔
871
  jsonObject.insert( QStringLiteral( "api_key" ), getApiKey( mApiRoot ) );
6✔
872
  jsonDoc.setObject( jsonObject );
3✔
873
  QByteArray json = jsonDoc.toJson( QJsonDocument::Compact );
3✔
874
  QNetworkReply *reply = mManager.post( request, json );
3✔
875
  connect( reply, &QNetworkReply::finished, this, [ = ]() { this->registrationFinished( username, password ); } );
6✔
876
  CoreUtils::log( "auth", QStringLiteral( "Requesting registration: " ) + url.toString() );
6✔
877
}
3✔
878

879
void MerginApi::getUserInfo()
20✔
880
{
881
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
20✔
882
  {
883
    return;
1✔
884
  }
885

886
  QString urlString;
19✔
887
  if ( mServerType == MerginServerType::OLD )
19✔
888
  {
889
    urlString = mApiRoot + QStringLiteral( "v1/user/%1" ).arg( mUserAuth->username() );
×
890
  }
891
  else
892
  {
893
    urlString = mApiRoot + QStringLiteral( "v1/user/profile" );
19✔
894
  }
895

896
  QNetworkRequest request = getDefaultRequest();
19✔
897
  QUrl url( urlString );
19✔
898
  request.setUrl( url );
19✔
899

900
  QNetworkReply *reply = mManager.get( request );
19✔
901
  CoreUtils::log( "user info", QStringLiteral( "Requesting user info: " ) + url.toString() );
38✔
902
  connect( reply, &QNetworkReply::finished, this, &MerginApi::getUserInfoFinished );
19✔
903
}
19✔
904

905
void MerginApi::getWorkspaceInfo()
25✔
906
{
907
  if ( mServerType == MerginServerType::OLD )
25✔
908
  {
909
    return;
×
910
  }
911

912
  if ( mUserInfo->activeWorkspaceId() == -1 )
25✔
913
  {
914
    return;
×
915
  }
916

917
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
25✔
918
  {
919
    return;
×
920
  }
921

922
  QString urlString = mApiRoot + QStringLiteral( "v1/workspace/%1" ).arg( mUserInfo->activeWorkspaceId() );
50✔
923
  QNetworkRequest request = getDefaultRequest();
25✔
924
  QUrl url( urlString );
25✔
925
  request.setUrl( url );
25✔
926

927
  QNetworkReply *reply = mManager.get( request );
25✔
928
  CoreUtils::log( "workspace info", QStringLiteral( "Requesting workspace info: " ) + url.toString() );
50✔
929
  connect( reply, &QNetworkReply::finished, this, &MerginApi::getWorkspaceInfoReplyFinished );
25✔
930
}
25✔
931

932
void MerginApi::getServiceInfo()
34✔
933
{
934
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
34✔
935
  {
936
    return;
×
937
  }
938

939
  QString urlString;
34✔
940

941
  if ( mServerType == MerginServerType::SAAS )
34✔
942
  {
943
    urlString = mApiRoot + QStringLiteral( "v1/workspace/%1/service" ).arg( mUserInfo->activeWorkspaceId() );
34✔
944
  }
945
  else if ( mServerType == MerginServerType::OLD )
×
946
  {
947
    urlString = mApiRoot + QStringLiteral( "v1/user/service" );
×
948
  }
949
  else
950
  {
951
    return;
×
952
  }
953

954
  QNetworkRequest request = getDefaultRequest( true );
34✔
955
  QUrl url( urlString );
34✔
956
  request.setUrl( url );
34✔
957

958
  QNetworkReply *reply = mManager.get( request );
34✔
959

960
  connect( reply, &QNetworkReply::finished, this, &MerginApi::getServiceInfoReplyFinished );
34✔
961

962
  CoreUtils::log( "Service info", QStringLiteral( "Requesting service info: " ) + url.toString() );
68✔
963
}
34✔
964

965
void MerginApi::getServiceInfoReplyFinished()
33✔
966
{
967
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
33✔
968
  Q_ASSERT( r );
33✔
969

970
  if ( r->error() == QNetworkReply::NoError )
33✔
971
  {
972
    CoreUtils::log( "Service info", QStringLiteral( "Success" ) );
33✔
973

974
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
33✔
975
    if ( doc.isObject() )
33✔
976
    {
977
      QJsonObject docObj = doc.object();
33✔
978
      mSubscriptionInfo->setFromJson( docObj );
33✔
979
    }
33✔
980
  }
33✔
981
  else
982
  {
983
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
984
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "getServiceInfo" ), r->errorString(), serverMsg );
×
985
    CoreUtils::log( "Service info", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
986

987
    mSubscriptionInfo->clear();
×
988

989
    int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
×
990
    if ( httpCode == 404 )
×
991
    {
992
      // no such API on the server, do not emit anything
993
    }
994
    else if ( httpCode == 403 )
×
995
    {
996
      // forbidden - I do not have enough rights to see this, do not emit anything
997
    }
998
    else
999
    {
1000
      emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: getServiceInfo" ) );
×
1001
    }
1002
  }
×
1003

1004
  r->deleteLater();
33✔
1005
}
33✔
1006

1007
void MerginApi::clearAuth()
3✔
1008
{
1009
  mUserAuth->clear();
3✔
1010
  mUserInfo->clear();
3✔
1011
  mUserInfo->clearCachedWorkspacesInfo();
3✔
1012
  mWorkspaceInfo->clear();
3✔
1013
  mSubscriptionInfo->clear();
3✔
1014
}
3✔
1015

1016
void MerginApi::resetApiRoot()
×
1017
{
1018
  QSettings settings;
×
1019
  settings.beginGroup( QStringLiteral( "Input/" ) );
×
1020
  setApiRoot( defaultApiRoot() );
×
1021
  settings.endGroup();
×
1022
}
×
1023

1024
QString MerginApi::resetPasswordUrl()
×
1025
{
1026
  if ( !mApiRoot.isEmpty() )
×
1027
  {
1028
    QUrl base( mApiRoot );
×
1029
    return base.resolved( QUrl( "login/reset" ) ).toString();
×
1030
  }
×
1031
  return QString();
×
1032
}
1033

1034
bool MerginApi::createProject( const QString &projectNamespace, const QString &projectName, bool isPublic )
40✔
1035
{
1036
  if ( !validateAuth() )
40✔
1037
  {
1038
    emit missingAuthorizationError( projectName );
×
1039
    return false;
×
1040
  }
1041

1042
  if ( mApiVersionStatus != MerginApiStatus::OK )
40✔
1043
  {
1044
    return false;
×
1045
  }
1046

1047
  QString projectFullName = getFullProjectName( projectNamespace, projectName );
80✔
1048

1049
  QNetworkRequest request = getDefaultRequest();
40✔
1050
  QUrl url( mApiRoot + QString( "/v1/project/%1" ).arg( projectNamespace ) );
80✔
1051
  request.setUrl( url );
40✔
1052
  request.setRawHeader( "Content-Type", "application/json" );
40✔
1053
  request.setRawHeader( "Accept", "application/json" );
40✔
1054
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
40✔
1055

1056
  QJsonDocument jsonDoc;
40✔
1057
  QJsonObject jsonObject;
40✔
1058
  jsonObject.insert( QStringLiteral( "name" ), projectName );
80✔
1059
  jsonObject.insert( QStringLiteral( "public" ), isPublic );
80✔
1060
  jsonDoc.setObject( jsonObject );
40✔
1061
  QByteArray json = jsonDoc.toJson( QJsonDocument::Compact );
40✔
1062

1063
  QNetworkReply *reply = mManager.post( request, json );
40✔
1064
  connect( reply, &QNetworkReply::finished, this, &MerginApi::createProjectFinished );
40✔
1065
  CoreUtils::log( "create " + projectFullName, QStringLiteral( "Requesting project creation: " ) + url.toString() );
80✔
1066

1067
  return true;
40✔
1068
}
40✔
1069

1070
void MerginApi::deleteProject( const QString &projectNamespace, const QString &projectName, bool informUser )
43✔
1071
{
1072
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
43✔
1073
  {
1074
    return;
1✔
1075
  }
1076

1077
  QString projectFullName = getFullProjectName( projectNamespace, projectName );
84✔
1078

1079
  QNetworkRequest request = getDefaultRequest();
42✔
1080
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/%1" ).arg( projectFullName ) );
84✔
1081
  request.setUrl( url );
42✔
1082
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
42✔
1083
  QNetworkReply *reply = mManager.deleteResource( request );
42✔
1084
  connect( reply, &QNetworkReply::finished, this, [this, informUser]() { this->deleteProjectFinished( informUser );} );
84✔
1085
  CoreUtils::log( "delete " + projectFullName, QStringLiteral( "Requesting project deletion: " ) + url.toString() );
84✔
1086
}
42✔
1087

1088
void MerginApi::saveAuthData()
12✔
1089
{
1090
  QSettings settings;
12✔
1091
  settings.beginGroup( "Input/" );
12✔
1092
  settings.setValue( "apiRoot", mApiRoot );
12✔
1093
  settings.endGroup();
12✔
1094

1095
  mUserAuth->saveAuthData();
12✔
1096
  mUserInfo->clear();
12✔
1097
}
12✔
1098

1099
void MerginApi::createProjectFinished()
40✔
1100
{
1101
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
40✔
1102
  Q_ASSERT( r );
40✔
1103

1104
  QString projectFullName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ) ).toString();
80✔
1105

1106
  QString projectNamespace, projectName;
40✔
1107
  extractProjectName( projectFullName, projectNamespace, projectName );
40✔
1108

1109
  if ( r->error() == QNetworkReply::NoError )
40✔
1110
  {
1111
    CoreUtils::log( "create " + projectFullName, QStringLiteral( "Success" ) );
39✔
1112
    emit projectCreated( projectFullName, true );
39✔
1113

1114

1115
    // Upload data if createProject has been called for a local project with empty namespace (case of migrating a project)
1116
    for ( const LocalProject &info : mLocalProjects.projects() )
433✔
1117
    {
1118
      if ( info.projectName == projectName && info.projectNamespace.isEmpty() )
394✔
1119
      {
1120
        mLocalProjects.updateNamespace( info.projectDir, projectNamespace );
3✔
1121
        emit projectAttachedToMergin( projectFullName, projectName );
3✔
1122

1123
        QDir projectDir( info.projectDir );
3✔
1124
        if ( projectDir.exists() && !projectDir.isEmpty() )
3✔
1125
        {
1126
          pushProject( projectNamespace, projectName, true );
3✔
1127
        }
1128
      }
3✔
1129
    }
39✔
1130
  }
1131
  else
1132
  {
1133
    QByteArray data = r->readAll();
1✔
1134
    QString code = extractServerErrorCode( data );
1✔
1135
    QString serverMsg = extractServerErrorMsg( data );
1✔
1136
    QString message = QStringLiteral( "FAILED - %1: %2" ).arg( r->errorString(), serverMsg );
2✔
1137
    bool showLimitReachedDialog = EnumHelper::isEqual( code, ErrorCode::ProjectsLimitHit );
1✔
1138

1139
    CoreUtils::log( "create " + projectFullName, message );
1✔
1140

1141
    emit projectCreated( projectFullName, false );
1✔
1142

1143
    if ( showLimitReachedDialog )
1✔
1144
    {
1145
      int maxProjects = 0;
×
1146
      QVariant maxProjectVariant = extractServerErrorValue( data, "projects_quota" );
×
1147
      if ( maxProjectVariant.isValid() )
×
1148
        maxProjects = maxProjectVariant.toInt();
×
1149
      emit projectLimitReached( maxProjects, serverMsg );
×
1150
    }
×
1151
    else
1152
    {
1153
      int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
1✔
1154
      emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: createProject" ), httpCode, projectName );
1✔
1155
    }
1156
  }
1✔
1157
  r->deleteLater();
40✔
1158
}
40✔
1159

1160
void MerginApi::deleteProjectFinished( bool informUser )
42✔
1161
{
1162
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
42✔
1163
  Q_ASSERT( r );
42✔
1164

1165
  QString projectFullName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ) ).toString();
84✔
1166

1167
  if ( r->error() == QNetworkReply::NoError )
42✔
1168
  {
1169
    CoreUtils::log( "delete " + projectFullName, QStringLiteral( "Success" ) );
2✔
1170

1171
    if ( informUser )
2✔
1172
      emit notify( QStringLiteral( "Project deleted" ) );
2✔
1173

1174
    emit serverProjectDeleted( projectFullName, true );
2✔
1175
  }
1176
  else
1177
  {
1178
    QString serverMsg = extractServerErrorMsg( r->readAll() );
80✔
1179
    CoreUtils::log( "delete " + projectFullName, QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) );
40✔
1180
    emit serverProjectDeleted( projectFullName, false );
40✔
1181
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: deleteProject" ) );
80✔
1182
  }
40✔
1183
  r->deleteLater();
42✔
1184
}
42✔
1185

1186
void MerginApi::authorizeFinished()
8✔
1187
{
1188
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
8✔
1189
  Q_ASSERT( r );
8✔
1190

1191
  if ( r->error() == QNetworkReply::NoError )
8✔
1192
  {
1193
    CoreUtils::log( "auth", QStringLiteral( "Success" ) );
8✔
1194
    const QByteArray data = r->readAll();
8✔
1195
    QJsonDocument doc = QJsonDocument::fromJson( data );
8✔
1196
    if ( doc.isObject() )
8✔
1197
    {
1198
      QJsonObject docObj = doc.object();
8✔
1199
      mUserAuth->setFromJson( docObj );
8✔
1200
    }
8✔
1201
    else
1202
    {
1203
      // keep username and password, but clear token
1204
      // this is problem with internet connection or server
1205
      // so do not force user to input login credentials again
1206
      mUserAuth->clearTokenData();
×
1207
      emit authFailed();
×
1208
      CoreUtils::log( "Auth", QStringLiteral( "FAILED - invalid JSON response" ) );
×
1209
      emit notify( "Internal server error during authorization" );
×
1210
    }
1211
  }
8✔
1212
  else
1213
  {
1214
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
1215
    QVariant statusCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute );
×
1216
    int status = statusCode.toInt();
×
1217
    CoreUtils::log( "Auth", QStringLiteral( "FAILED - %1. %2 (%3)" ).arg( r->errorString(), serverMsg, QString::number( status ) ) );
×
1218

1219
    if ( status == 401 )
×
1220
    {
1221
      // OK, we have INVALID username or password or
1222
      // our user got blocked on the server by admin or owner
1223
      // lets show error to user and let him try different credentials
1224
      emit authFailed();
×
1225
      emit notify( serverMsg );
×
1226

1227
      mUserAuth->blockSignals( true );
×
1228
      mUserAuth->setUsername( QString() );
×
1229
      mUserAuth->setPassword( QString() );
×
1230
      mUserAuth->blockSignals( false );
×
1231

1232
    }
1233
    else
1234
    {
1235
      // keep username and password
1236
      // this is problem with internet connection or server
1237
      // so do not force user to input login credentials again
1238
      emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: authorize" ) );
×
1239
    }
1240

1241
    // in case of any error, just clean token and request new one
1242
    mUserAuth->clearTokenData();
×
1243
  }
×
1244

1245
  if ( mAuthLoopEvent.isRunning() )
8✔
1246
  {
1247
    mAuthLoopEvent.exit();
1✔
1248
  }
1249
  r->deleteLater();
8✔
1250
}
8✔
1251

1252
void MerginApi::registrationFinished( const QString &username, const QString &password )
3✔
1253
{
1254
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
3✔
1255
  Q_ASSERT( r );
3✔
1256

1257
  if ( r->error() == QNetworkReply::NoError )
3✔
1258
  {
1259
    CoreUtils::log( "register", QStringLiteral( "Success" ) );
3✔
1260
    QString msg = tr( "Registration successful" );
3✔
1261
    emit notify( msg );
3✔
1262

1263
    if ( !username.isEmpty() && !password.isEmpty() ) // log in immediately
3✔
1264
      authorize( username, password );
3✔
1265

1266
    emit registrationSucceeded();
3✔
1267
  }
3✔
1268
  else
1269
  {
1270
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
1271
    CoreUtils::log( "register", QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) );
×
1272
    QVariant statusCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute );
×
1273
    int status = statusCode.toInt();
×
1274
    if ( status == 401 || status == 400 )
×
1275
    {
1276
      emit registrationFailed( serverMsg, RegistrationError::RegistrationErrorType::OTHER );
×
1277
      emit notify( serverMsg );
×
1278
    }
1279
    else if ( status == 404 )
×
1280
    {
1281
      // the self-registration is not allowed on the server
1282
      QString msg = tr( "New registrations are not allowed on the selected Mergin server.%1Please check with your administrator." ).arg( "\n" );
×
1283
      emit registrationFailed( msg, RegistrationError::RegistrationErrorType::OTHER );
×
1284
      emit notify( msg );
×
1285
    }
×
1286
    else
1287
    {
1288
      QString msg = QStringLiteral( "Mergin API error: register" );
×
1289
      emit registrationFailed( msg, RegistrationError::RegistrationErrorType::OTHER );
×
1290
      emit networkErrorOccurred( serverMsg, msg );
×
1291
    }
×
1292
  }
×
1293
  r->deleteLater();
3✔
1294
}
3✔
1295

1296
void MerginApi::pingMerginReplyFinished()
11✔
1297
{
1298
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
11✔
1299
  Q_ASSERT( r );
11✔
1300
  QString apiVersion;
11✔
1301
  QString serverMsg;
11✔
1302
  bool serverSupportsSubscriptions = false;
11✔
1303

1304
  if ( r->error() == QNetworkReply::NoError )
11✔
1305
  {
1306
    CoreUtils::log( "ping", QStringLiteral( "Success" ) );
11✔
1307
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
11✔
1308
    if ( doc.isObject() )
11✔
1309
    {
1310
      QJsonObject obj = doc.object();
11✔
1311
      apiVersion = obj.value( QStringLiteral( "version" ) ).toString();
11✔
1312
      serverSupportsSubscriptions = obj.value( QStringLiteral( "subscriptions_enabled" ) ).toBool();
11✔
1313
    }
11✔
1314
  }
11✔
1315
  else
1316
  {
1317
    serverMsg = extractServerErrorMsg( r->readAll() );
×
1318
    CoreUtils::log( "ping", QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) );
×
1319
  }
1320
  r->deleteLater();
11✔
1321
  emit pingMerginFinished( apiVersion, serverSupportsSubscriptions, serverMsg );
11✔
1322
}
11✔
1323

1324
void MerginApi::onPlanProductIdChanged()
15✔
1325
{
1326
  if ( mUserAuth->hasAuthData() )
15✔
1327
  {
1328
    if ( mServerType == MerginServerType::OLD )
14✔
1329
    {
1330
      getUserInfo();
×
1331
    }
1332
    else
1333
    {
1334
      getWorkspaceInfo();
14✔
1335
    }
1336
  }
1337
}
15✔
1338

1339
QNetworkReply *MerginApi::getProjectInfo( const QString &projectFullName, bool withAuth )
243✔
1340
{
1341
  if ( withAuth && !validateAuth() )
243✔
1342
  {
1343
    emit missingAuthorizationError( projectFullName );
×
1344
    return nullptr;
×
1345
  }
1346

1347
  if ( mApiVersionStatus != MerginApiStatus::OK )
243✔
1348
  {
1349
    return nullptr;
×
1350
  }
1351

1352
  int sinceVersion = -1;
243✔
1353
  LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName );
243✔
1354
  if ( projectInfo.isValid() )
243✔
1355
  {
1356
    // let's also fetch the recent history of diffable files
1357
    // (the "since" is inclusive, so if we are on v2, we want to use since=v3 which will include v2->v3, v3->v4, ...)
1358
    sinceVersion = projectInfo.localVersion + 1;
180✔
1359
  }
1360

1361
  QUrlQuery query;
243✔
1362
  if ( sinceVersion != -1 )
243✔
1363
    query.addQueryItem( QStringLiteral( "since" ), QStringLiteral( "v%1" ).arg( sinceVersion ) );
360✔
1364

1365
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/%1" ).arg( projectFullName ) );
486✔
1366
  url.setQuery( query );
243✔
1367

1368
  QNetworkRequest request = getDefaultRequest( withAuth );
243✔
1369
  request.setUrl( url );
243✔
1370
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
243✔
1371

1372
  return mManager.get( request );
243✔
1373
}
243✔
1374

1375
bool MerginApi::validateAuth()
816✔
1376
{
1377
  if ( !mUserAuth->hasAuthData() )
816✔
1378
  {
1379
    emit authRequested();
×
1380
    return false;
×
1381
  }
1382

1383
  if ( mUserAuth->authToken().isEmpty() || mUserAuth->tokenExpiration() < QDateTime().currentDateTime().toUTC() )
816✔
1384
  {
1385
    authorize( mUserAuth->username(), mUserAuth->password() );
1✔
1386
    CoreUtils::log( QStringLiteral( "MerginApi" ), QStringLiteral( "Requesting authorization because of missing or expired token." ) );
2✔
1387
    mAuthLoopEvent.exec();
1✔
1388
  }
1389
  return true;
816✔
1390
}
1391

1392
void MerginApi::checkMerginVersion( QString apiVersion, bool serverSupportsSubscriptions, QString msg )
11✔
1393
{
1394
  setApiSupportsSubscriptions( serverSupportsSubscriptions );
11✔
1395

1396
  if ( msg.isEmpty() )
11✔
1397
  {
1398
    int major = -1;
11✔
1399
    int minor = -1;
11✔
1400
    QRegularExpression re;
11✔
1401
    re.setPattern( QStringLiteral( "(?<major>\\d+)[.](?<minor>\\d+)" ) );
11✔
1402
    QRegularExpressionMatch match = re.match( apiVersion );
11✔
1403
    if ( match.hasMatch() )
11✔
1404
    {
1405
      major = match.captured( "major" ).toInt();
11✔
1406
      minor = match.captured( "minor" ).toInt();
11✔
1407
    }
1408

1409
    if ( ( MERGIN_API_VERSION_MAJOR == major && MERGIN_API_VERSION_MINOR <= minor ) || ( MERGIN_API_VERSION_MAJOR < major ) )
11✔
1410
    {
1411
      setApiVersionStatus( MerginApiStatus::OK );
11✔
1412
    }
1413
    else
1414
    {
1415
      setApiVersionStatus( MerginApiStatus::INCOMPATIBLE );
×
1416
    }
1417
  }
11✔
1418
  else
1419
  {
1420
    setApiVersionStatus( MerginApiStatus::NOT_FOUND );
×
1421
  }
1422
}
11✔
1423

1424
bool MerginApi::extractProjectName( const QString &sourceString, QString &projectNamespace, QString &name )
189✔
1425
{
1426
  QStringList parts = sourceString.split( "/" );
189✔
1427
  if ( parts.length() > 1 )
189✔
1428
  {
1429
    projectNamespace = parts.at( parts.length() - 2 );
189✔
1430
    name = parts.last();
189✔
1431
    return true;
189✔
1432
  }
1433
  else
1434
  {
1435
    name = sourceString;
×
1436
    return false;
×
1437
  }
1438
}
189✔
1439

1440
QString MerginApi::extractServerErrorCode( const QByteArray &data )
1✔
1441
{
1442
  QVariant code = extractServerErrorValue( data, QStringLiteral( "code" ) );
2✔
1443
  if ( code.isValid() )
1✔
1444
    return code.toString();
×
1445
  return QString();
1✔
1446
}
1✔
1447

1448
QVariant MerginApi::extractServerErrorValue( const QByteArray &data, const QString &key )
1✔
1449
{
1450
  QJsonDocument doc = QJsonDocument::fromJson( data );
1✔
1451
  if ( doc.isObject() )
1✔
1452
  {
1453
    QJsonObject obj = doc.object();
1✔
1454
    if ( obj.contains( key ) )
1✔
1455
    {
1456
      QJsonValue val = obj.value( key );
×
1457
      return val.toVariant();
×
1458
    }
×
1459
  }
1✔
1460

1461
  return QVariant();
1✔
1462
}
1✔
1463

1464
QString MerginApi::extractServerErrorMsg( const QByteArray &data )
50✔
1465
{
1466
  QString serverMsg = "[can't parse server error]";
50✔
1467
  QJsonDocument doc = QJsonDocument::fromJson( data );
50✔
1468
  if ( doc.isObject() )
50✔
1469
  {
1470
    QJsonObject obj = doc.object();
44✔
1471
    if ( obj.contains( QStringLiteral( "detail" ) ) )
44✔
1472
    {
1473
      QJsonValue vDetail = obj.value( "detail" );
42✔
1474
      if ( vDetail.isString() )
42✔
1475
      {
1476
        serverMsg = vDetail.toString();
42✔
1477
      }
1478
      else if ( vDetail.isObject() )
×
1479
      {
1480
        serverMsg = QJsonDocument( vDetail.toObject() ).toJson();
×
1481
      }
1482
    }
42✔
1483
    else if ( obj.contains( QStringLiteral( "name" ) ) )
2✔
1484
    {
1485
      QJsonValue val = obj.value( "name" );
2✔
1486
      if ( val.isArray() )
2✔
1487
      {
1488
        QJsonArray errors = val.toArray();
1✔
1489
        QStringList messages;
1✔
1490
        for ( auto it = errors.constBegin(); it != errors.constEnd(); ++it )
2✔
1491
        {
1492
          messages << it->toString();
1✔
1493
        }
1494
        serverMsg = messages.join( " " );
1✔
1495
      }
1✔
1496
    }
2✔
1497
    else
1498
    {
1499
      serverMsg = "[can't parse server error]";
×
1500
    }
1501
  }
44✔
1502
  else
1503
  {
1504
    // take only first 1000 bytes of the message ~ there are situations when data is an unclosed string that would eat the whole log memory
1505
    serverMsg = data.mid( 0, 1000 );
6✔
1506
  }
1507

1508
  return serverMsg;
100✔
1509
}
50✔
1510

1511

1512
LocalProject MerginApi::getLocalProject( const QString &projectFullName )
7✔
1513
{
1514
  return mLocalProjects.projectFromMerginName( projectFullName );
7✔
1515
}
1516

1517
ProjectDiff MerginApi::localProjectChanges( const QString &projectDir )
17✔
1518
{
1519
  MerginProjectMetadata projectMetadata = MerginProjectMetadata::fromCachedJson( projectDir + "/" + sMetadataFile );
34✔
1520
  QList<MerginFile> localFiles = getLocalProjectFiles( projectDir + "/" );
17✔
1521

1522
  MerginConfig config = MerginConfig::fromFile( projectDir + "/" + sMerginConfigFile );
34✔
1523

1524
  return compareProjectFiles( projectMetadata.files, projectMetadata.files, localFiles, projectDir, config.isValid, config );
34✔
1525
}
17✔
1526

1527
bool MerginApi::hasLocalProjectChanges( const QString &projectDir )
328✔
1528
{
1529
  MerginProjectMetadata projectMetadata = MerginProjectMetadata::fromCachedJson( projectDir + "/" + sMetadataFile );
656✔
1530
  QList<MerginFile> localFiles = getLocalProjectFiles( projectDir + "/" );
328✔
1531

1532
  MerginConfig config = MerginConfig::fromFile( projectDir + "/" + sMerginConfigFile );
656✔
1533

1534
  return hasLocalChanges( projectMetadata.files, localFiles, projectDir );
656✔
1535
}
328✔
1536

1537
QString MerginApi::getTempProjectDir( const QString &projectFullName )
337✔
1538
{
1539
  return mDataDir + "/" + TEMP_FOLDER + projectFullName;
674✔
1540
}
1541

1542
QString MerginApi::getFullProjectName( QString projectNamespace, QString projectName ) // TODO: move to inpututils?
43,979✔
1543
{
1544
  return QString( "%1/%2" ).arg( projectNamespace ).arg( projectName );
43,979✔
1545
}
1546

1547
MerginApiStatus::VersionStatus MerginApi::apiVersionStatus() const
38✔
1548
{
1549
  return mApiVersionStatus;
38✔
1550
}
1551

1552
void MerginApi::setApiVersionStatus( const MerginApiStatus::VersionStatus &apiVersionStatus )
35✔
1553
{
1554
  if ( mApiVersionStatus != apiVersionStatus )
35✔
1555
  {
1556
    mApiVersionStatus = apiVersionStatus;
33✔
1557
    emit apiVersionStatusChanged();
33✔
1558
  }
1559
}
35✔
1560

1561
void MerginApi::pingMergin()
24✔
1562
{
1563
  if ( mApiVersionStatus == MerginApiStatus::OK ) return;
24✔
1564

1565
  setApiVersionStatus( MerginApiStatus::PENDING );
24✔
1566

1567
  QNetworkRequest request = getDefaultRequest( false );
24✔
1568
  QUrl url( mApiRoot + QStringLiteral( "/ping" ) );
48✔
1569
  request.setUrl( url );
24✔
1570

1571
  QNetworkReply *reply = mManager.get( request );
24✔
1572
  CoreUtils::log( "ping", QStringLiteral( "Requesting: " ) + url.toString() );
48✔
1573
  connect( reply, &QNetworkReply::finished, this, &MerginApi::pingMerginReplyFinished );
24✔
1574
}
24✔
1575

1576
void MerginApi::migrateProjectToMergin( const QString &projectName, const QString &projectNamespace )
3✔
1577
{
1578
  CoreUtils::log( "migrate project", projectName );
3✔
1579
  if ( projectNamespace.isEmpty() )
3✔
1580
  {
1581
    createProject( mUserAuth->username(), projectName );
3✔
1582
  }
1583
  else
1584
  {
1585
    createProject( projectNamespace, projectName );
×
1586
  }
1587
}
3✔
1588

1589
void MerginApi::detachProjectFromMergin( const QString &projectNamespace, const QString &projectName, bool informUser )
1✔
1590
{
1591
  // Remove mergin folder
1592
  QString projectFullName = getFullProjectName( projectNamespace, projectName );
2✔
1593
  LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName );
1✔
1594

1595
  if ( projectInfo.isValid() )
1✔
1596
  {
1597
    CoreUtils::removeDir( projectInfo.projectDir + "/.mergin" );
1✔
1598
  }
1599

1600
  // Update localProject
1601
  mLocalProjects.updateNamespace( projectInfo.projectDir, "" );
1✔
1602
  mLocalProjects.updateLocalVersion( projectInfo.projectDir, -1 );
1✔
1603

1604
  if ( informUser )
1✔
1605
    emit notify( tr( "Project detached from Mergin" ) );
1✔
1606

1607
  emit projectDetached( projectFullName );
1✔
1608
}
1✔
1609

1610
QString MerginApi::apiRoot() const
131✔
1611
{
1612
  return mApiRoot;
131✔
1613
}
1614

1615
void MerginApi::setApiRoot( const QString &apiRoot )
4✔
1616
{
1617
  QString newApiRoot;
4✔
1618
  if ( apiRoot.isEmpty() )
4✔
1619
  {
1620
    newApiRoot = defaultApiRoot();
×
1621
  }
1622
  else
1623
  {
1624
    newApiRoot = apiRoot;
4✔
1625
  }
1626

1627
  if ( newApiRoot != mApiRoot )
4✔
1628
  {
1629
    mApiRoot = newApiRoot;
2✔
1630

1631
    QSettings settings;
2✔
1632
    settings.setValue( QStringLiteral( "Input/apiRoot" ), mApiRoot );
4✔
1633

1634
    emit apiRootChanged();
2✔
1635
  }
2✔
1636
}
4✔
1637

1638
QString MerginApi::merginUserName() const
29✔
1639
{
1640
  return userAuth()->username();
29✔
1641
}
1642

1643
QList<MerginFile> MerginApi::getLocalProjectFiles( const QString &projectPath )
585✔
1644
{
1645
  QElapsedTimer timer;
585✔
1646
  timer.start();
585✔
1647

1648
  QList<MerginFile> merginFiles;
585✔
1649
  ProjectChecksumCache checksumCache( projectPath );
585✔
1650

1651
  QSet<QString> localFiles = listFiles( projectPath );
585✔
1652
  for ( QString p : localFiles )
2,244✔
1653
  {
1654
    MerginFile file;
1,659✔
1655
    file.checksum = checksumCache.get( p );
1,659✔
1656
    file.path = p;
1,659✔
1657
    QFileInfo info( projectPath + p );
1,659✔
1658
    file.size = info.size();
1,659✔
1659
    file.mtime = info.lastModified();
1,659✔
1660
    merginFiles.append( file );
1,659✔
1661
  }
1,659✔
1662

1663
  qint64 elapsed = timer.elapsed();
585✔
1664
  if ( elapsed > 100 )
585✔
1665
  {
1666
    CoreUtils::log( "Local File", QStringLiteral( "It took %1 ms to create MerginFiles for %2 local files for %3." ).arg( elapsed ).arg( localFiles.count() ).arg( projectPath ) );
×
1667
  }
1668
  return merginFiles;
1,170✔
1669
}
585✔
1670

1671
void MerginApi::listProjectsReplyFinished( QString requestId )
22✔
1672
{
1673
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
22✔
1674
  Q_ASSERT( r );
22✔
1675

1676
  int projectCount = -1;
22✔
1677
  int requestedPage = 1;
22✔
1678
  MerginProjectsList projectList;
22✔
1679

1680
  if ( r->error() == QNetworkReply::NoError )
22✔
1681
  {
1682
    QUrlQuery query( r->request().url().query() );
44✔
1683
    requestedPage = query.queryItemValue( "page" ).toInt();
22✔
1684

1685
    QByteArray data = r->readAll();
22✔
1686
    QJsonDocument doc = QJsonDocument::fromJson( data );
22✔
1687

1688
    if ( doc.isObject() )
22✔
1689
    {
1690
      projectCount = doc.object().value( "count" ).toInt();
22✔
1691
      projectList = parseProjectsFromJson( doc );
22✔
1692
    }
1693

1694
    CoreUtils::log( "list projects", QStringLiteral( "Success - got %1 projects" ).arg( projectList.count() ) );
22✔
1695
  }
22✔
1696
  else
1697
  {
1698
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
1699
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listProjects" ), r->errorString(), serverMsg );
×
1700
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: listProjects" ) );
×
1701
    CoreUtils::log( "list projects", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
1702

1703
    emit listProjectsFailed();
×
1704
  }
×
1705

1706
  r->deleteLater();
22✔
1707

1708
  emit listProjectsFinished( projectList, projectCount, requestedPage, requestId );
22✔
1709
}
22✔
1710

1711
void MerginApi::listProjectsByNameReplyFinished( QString requestId )
7✔
1712
{
1713
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
7✔
1714
  Q_ASSERT( r );
7✔
1715

1716
  MerginProjectsList projectList;
7✔
1717

1718
  if ( r->error() == QNetworkReply::NoError )
7✔
1719
  {
1720
    QByteArray data = r->readAll();
7✔
1721
    QJsonDocument json = QJsonDocument::fromJson( data );
7✔
1722
    projectList = parseProjectsFromJson( json );
7✔
1723
    CoreUtils::log( "list projects by name", QStringLiteral( "Success - got %1 projects" ).arg( projectList.count() ) );
7✔
1724
  }
7✔
1725
  else
1726
  {
1727
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
1728
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listProjectsByName" ), r->errorString(), serverMsg );
×
1729
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: listProjectsByName" ) );
×
1730
    CoreUtils::log( "list projects by name", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
1731

1732
    emit listProjectsFailed();
×
1733
  }
×
1734

1735
  r->deleteLater();
7✔
1736

1737
  emit listProjectsByNameFinished( projectList, requestId );
7✔
1738
}
7✔
1739

1740

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

1745
  QString dest = projectDir + "/" + filePath;
202✔
1746
  createPathIfNotExists( dest );
202✔
1747

1748
  QFile f( dest );
202✔
1749
  if ( !f.open( QIODevice::WriteOnly ) )
202✔
1750
  {
1751
    CoreUtils::log( "pull " + projectFullName, "Failed to open file for writing " + dest );
×
1752
    return;
×
1753
  }
1754

1755
  // assemble file from tmp files
1756
  for ( const auto &item : items )
343✔
1757
  {
1758
    QFile fTmp( tempDir + "/" + item.tempFileName );
282✔
1759
    if ( !fTmp.open( QIODevice::ReadOnly ) )
141✔
1760
    {
1761
      CoreUtils::log( "pull " + projectFullName, "Failed to open temp file for reading " + item.tempFileName );
×
1762
      return;
×
1763
    }
1764
    f.write( fTmp.readAll() );
141✔
1765
  }
141✔
1766

1767
  f.close();
202✔
1768

1769
  // if diffable, copy to .mergin dir so we have a basefile
1770
  if ( MerginApi::isFileDiffable( filePath ) )
202✔
1771
  {
1772
    QString basefile = projectDir + "/.mergin/" + filePath;
15✔
1773
    createPathIfNotExists( basefile );
15✔
1774

1775
    if ( !QFile::remove( basefile ) )
15✔
1776
    {
1777
      CoreUtils::log( "pull " + projectFullName, "failed to remove old basefile for: " + filePath );
15✔
1778
    }
1779
    if ( !QFile::copy( dest, basefile ) )
15✔
1780
    {
1781
      CoreUtils::log( "pull " + projectFullName, "failed to copy new basefile for: " + filePath );
×
1782
    }
1783
  }
15✔
1784
}
202✔
1785

1786

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

1791
  // update diffable files that have been modified on the server
1792
  // - if they were not modified locally, the server changes will be simply applied
1793
  // - if they were modified locally, local changes will be rebased on top of server changes
1794

1795
  QString src = tempDir + "/" + CoreUtils::uuidWithoutBraces( QUuid::createUuid() );
18✔
1796
  QString dest = projectDir + "/" + filePath;
9✔
1797
  QString basefile = projectDir + "/.mergin/" + filePath;
9✔
1798

1799
  LocalProject info = mLocalProjects.projectFromMerginName( projectFullName );
9✔
1800

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

1804
  createPathIfNotExists( src );
9✔
1805
  createPathIfNotExists( dest );
9✔
1806
  createPathIfNotExists( basefile );
9✔
1807

1808
  QStringList diffFiles;
9✔
1809
  for ( const auto &item : items )
19✔
1810
    diffFiles << tempDir + "/" + item.tempFileName;
10✔
1811

1812
  //
1813
  // let's first assemble server's file from our basefile + diffs
1814
  //
1815

1816
  if ( !QFile::copy( basefile, src ) )
9✔
1817
  {
1818
    CoreUtils::log( "pull " + projectFullName, "assemble server file fail: copying failed " + basefile + " to " + src );
×
1819

1820
    // TODO: this is a critical failure - we should abort pull
1821
  }
1822

1823
  if ( !GeodiffUtils::applyDiffs( src, diffFiles ) )
9✔
1824
  {
1825
    CoreUtils::log( "pull " + projectFullName, "server file assembly failed: " + filePath );
×
1826

1827
    // TODO: this is a critical failure - we should abort pull
1828
    // TODO: we could try to delete the basefile and re-download it from scratch on next sync
1829
  }
1830
  else
1831
  {
1832
    CoreUtils::log( "pull " + projectFullName, "server file assembly successful: " + filePath );
9✔
1833
  }
1834

1835
  //
1836
  // now we are ready for the update of our local file
1837
  //
1838
  bool hasConflicts = false;
9✔
1839

1840
  bool res = GeodiffUtils::rebase( basefile,
9✔
1841
                                   src,
1842
                                   dest,
1843
                                   conflictfile
1844
                                 );
1845
  if ( res )
9✔
1846
  {
1847
    CoreUtils::log( "pull " + projectFullName, "geodiff rebase successful: " + filePath );
8✔
1848
  }
1849
  else
1850
  {
1851
    CoreUtils::log( "pull " + projectFullName, "geodiff rebase failed! " + filePath );
1✔
1852

1853
    // not good... something went wrong in rebase - we need to save the local changes
1854
    // let's put them into a conflict file and use the server version
1855
    hasConflicts = true;
1✔
1856
    LocalProject info = mLocalProjects.projectFromMerginName( projectFullName );
1✔
1857
    QString newDest = CoreUtils::findUniquePath( CoreUtils::generateConflictedCopyFileName( dest, mUserAuth->username(), info.localVersion ) );
2✔
1858
    if ( !QFile::rename( dest, newDest ) )
1✔
1859
    {
1860
      CoreUtils::log( "pull " + projectFullName, "failed rename of conflicting file after failed geodiff rebase: " + filePath );
×
1861
    }
1862
    if ( !QFile::copy( src, dest ) )
1✔
1863
    {
1864
      CoreUtils::log( "pull " + projectFullName, "failed to update local conflicting file after failed geodiff rebase: " + filePath );
×
1865
    }
1866
  }
1✔
1867

1868
  //
1869
  // finally update our basefile
1870
  //
1871

1872
  if ( !QFile::remove( basefile ) )
9✔
1873
  {
1874
    CoreUtils::log( "pull " + projectFullName, "failed removal of old basefile: " + filePath );
×
1875

1876
    // TODO: this is a critical failure - we should abort pull
1877
  }
1878
  if ( !QFile::rename( src, basefile ) )
9✔
1879
  {
1880
    CoreUtils::log( "pull " + projectFullName, "failed rename of basefile using new server content: " + filePath );
×
1881

1882
    // TODO: this is a critical failure - we should abort pull
1883
  }
1884
  return hasConflicts;
9✔
1885
}
9✔
1886

1887
void MerginApi::finalizeProjectPull( const QString &projectFullName )
123✔
1888
{
1889
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
123✔
1890
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
123✔
1891

1892
  QString projectDir = transaction.projectDir;
123✔
1893
  QString tempProjectDir = getTempProjectDir( projectFullName );
123✔
1894

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

1897
  for ( const PullTask &finalizationItem : transaction.pullTasks )
337✔
1898
  {
1899
    switch ( finalizationItem.method )
214✔
1900
    {
1901
      case PullTask::Copy:
199✔
1902
      {
1903
        finalizeProjectPullCopy( projectFullName, projectDir, tempProjectDir, finalizationItem.filePath, finalizationItem.data );
199✔
1904
        break;
199✔
1905
      }
1906

1907
      case PullTask::CopyConflict:
3✔
1908
      {
1909
        // move local file to conflict file
1910
        QString origPath = projectDir + "/" + finalizationItem.filePath;
3✔
1911
        LocalProject info = mLocalProjects.projectFromMerginName( projectFullName );
3✔
1912
        QString newPath = CoreUtils::findUniquePath( CoreUtils::generateConflictedCopyFileName( origPath, mUserAuth->username(), info.localVersion ) );
6✔
1913
        if ( !QFile::rename( origPath, newPath ) )
3✔
1914
        {
1915
          CoreUtils::log( "pull " + projectFullName, "failed rename of conflicting file: " + finalizationItem.filePath );
×
1916
        }
1917
        else
1918
        {
1919
          CoreUtils::log( "pull " + projectFullName, "Local file renamed due to conflict with server: " + finalizationItem.filePath );
3✔
1920
        }
1921
        finalizeProjectPullCopy( projectFullName, projectDir, tempProjectDir, finalizationItem.filePath, finalizationItem.data );
3✔
1922
        break;
3✔
1923
      }
3✔
1924

1925
      case PullTask::ApplyDiff:
9✔
1926
      {
1927
        // applying diff can result in conflicted copy too, in this case
1928
        // we need to update gpkgSchemaChanged flag.
1929
        bool res = finalizeProjectPullApplyDiff( projectFullName, projectDir, tempProjectDir, finalizationItem.filePath, finalizationItem.data );
9✔
1930
        transaction.gpkgSchemaChanged = res;
9✔
1931
        break;
9✔
1932
      }
1933

1934
      case PullTask::Delete:
3✔
1935
      {
1936
        CoreUtils::log( "pull " + projectFullName, "Removing local file: " + finalizationItem.filePath );
3✔
1937
        QFile file( projectDir + "/" + finalizationItem.filePath );
6✔
1938
        file.remove();
3✔
1939
        break;
3✔
1940
      }
3✔
1941
    }
1942

1943
    // remove tmp files associated with this item
1944
    for ( const auto &downloadItem : finalizationItem.data )
365✔
1945
    {
1946
      if ( !QFile::remove( tempProjectDir + "/" + downloadItem.tempFileName ) )
151✔
1947
        CoreUtils::log( "pull " + projectFullName, "Failed to remove temporary file " + downloadItem.tempFileName );
×
1948
    }
1949
  }
1950

1951
  // check there are no files left
1952
  int tmpFilesLeft = QDir( tempProjectDir ).entryList( QDir::NoDotAndDotDot ).count();
123✔
1953
  if ( tmpFilesLeft )
123✔
1954
  {
1955
    CoreUtils::log( "pull " + projectFullName, "Some temporary files were left - this should not happen..." );
×
1956
  }
1957

1958
  QDir( tempProjectDir ).removeRecursively();
123✔
1959

1960
  // add the local project if not there yet
1961
  if ( !mLocalProjects.projectFromMerginName( projectFullName ).isValid() )
123✔
1962
  {
1963
    QString projectNamespace, projectName;
61✔
1964
    extractProjectName( projectFullName, projectNamespace, projectName );
61✔
1965

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

1970
    mLocalProjects.addMerginProject( projectDir, projectNamespace, projectName );
61✔
1971
  }
61✔
1972

1973
  finishProjectSync( projectFullName, true );
123✔
1974
}
123✔
1975

1976

1977
void MerginApi::pushStartReplyFinished()
115✔
1978
{
1979
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
115✔
1980
  Q_ASSERT( r );
115✔
1981

1982
  QString projectFullName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ) ).toString();
230✔
1983

1984
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
115✔
1985
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
115✔
1986
  Q_ASSERT( r == transaction.replyPushStart );
115✔
1987

1988
  if ( r->error() == QNetworkReply::NoError )
115✔
1989
  {
1990
    QByteArray data = r->readAll();
115✔
1991

1992
    transaction.replyPushStart->deleteLater();
115✔
1993
    transaction.replyPushStart = nullptr;
115✔
1994

1995
    QList<MerginFile> files = transaction.pushQueue;
115✔
1996
    if ( !files.isEmpty() )
115✔
1997
    {
1998
      QString transactionUUID;
111✔
1999
      QJsonDocument doc = QJsonDocument::fromJson( data );
111✔
2000
      if ( doc.isObject() )
111✔
2001
      {
2002
        QJsonObject docObj = doc.object();
111✔
2003
        transactionUUID = docObj.value( QStringLiteral( "transaction" ) ).toString();
111✔
2004
        transaction.transactionUUID = transactionUUID;
111✔
2005
      }
111✔
2006

2007
      if ( transaction.transactionUUID.isEmpty() )
111✔
2008
      {
2009
        CoreUtils::log( "push " + projectFullName, QStringLiteral( "Fail! Could not acquire transaction ID" ) );
×
2010
        finishProjectSync( projectFullName, false );
×
2011
      }
2012

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

2015
      MerginFile file = files.first();
111✔
2016
      pushFile( projectFullName, transactionUUID, file );
111✔
2017
      emit pushFilesStarted();
111✔
2018
    }
111✔
2019
    else  // pushing only files to be removed
2020
    {
2021
      // we are done here - no upload of chunks, no request to "finish"
2022
      // because server immediatelly creates a new version without starting a transaction to upload chunks
2023

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

2026
      transaction.projectMetadata = data;
4✔
2027
      transaction.version = MerginProjectMetadata::fromJson( data ).version;
4✔
2028

2029
      finishProjectSync( projectFullName, true );
4✔
2030
    }
2031
  }
115✔
2032
  else
2033
  {
2034
    QByteArray data = r->readAll();
×
2035
    QString serverMsg = extractServerErrorMsg( data );
×
2036
    QString code = extractServerErrorCode( data );
×
2037
    bool showLimitReachedDialog = EnumHelper::isEqual( code, ErrorCode::StorageLimitHit );
×
2038

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

2041
    transaction.replyPushStart->deleteLater();
×
2042
    transaction.replyPushStart = nullptr;
×
2043

2044
    if ( showLimitReachedDialog )
×
2045
    {
2046
      const QList<MerginFile> files = transaction.pushQueue;
×
2047
      qreal uploadSize = 0;
×
2048
      for ( const MerginFile &f : files )
×
2049
      {
2050
        uploadSize += f.size;
×
2051
      }
2052
      emit storageLimitReached( uploadSize );
×
2053

2054
      // remove project if it was first time sync - migration
2055
      if ( transaction.isInitialPush )
×
2056
      {
2057
        QString projectNamespace, projectName;
×
2058
        extractProjectName( projectFullName, projectNamespace, projectName );
×
2059

2060
        detachProjectFromMergin( projectNamespace, projectName, false );
×
2061
        deleteProject( projectNamespace, projectName, false );
×
2062
      }
×
2063
    }
×
2064
    else
2065
    {
2066
      int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
×
2067
      emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: pushStartReply" ), httpCode, projectFullName );
×
2068
    }
2069
    finishProjectSync( projectFullName, false );
×
2070
  }
×
2071
}
115✔
2072

2073
void MerginApi::pushFileReplyFinished()
152✔
2074
{
2075
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
152✔
2076
  Q_ASSERT( r );
152✔
2077

2078
  QString projectFullName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ) ).toString();
304✔
2079

2080
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
152✔
2081
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
152✔
2082
  Q_ASSERT( r == transaction.replyPushFile );
152✔
2083

2084
  QStringList params = ( r->url().toString().split( "/" ) );
304✔
2085
  QString transactionUUID = params.at( params.length() - 2 );
152✔
2086
  QString chunkID = params.at( params.length() - 1 );
152✔
2087
  Q_ASSERT( transactionUUID == transaction.transactionUUID );
152✔
2088

2089
  if ( r->error() == QNetworkReply::NoError )
152✔
2090
  {
2091
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Uploaded successfully: " ) + chunkID );
151✔
2092

2093
    transaction.replyPushFile->deleteLater();
151✔
2094
    transaction.replyPushFile = nullptr;
151✔
2095

2096
    MerginFile currentFile = transaction.pushQueue.first();
151✔
2097
    int chunkNo = currentFile.chunks.indexOf( chunkID );
151✔
2098
    if ( chunkNo < currentFile.chunks.size() - 1 )
151✔
2099
    {
2100
      pushFile( projectFullName, transactionUUID, currentFile, chunkNo + 1 );
2✔
2101
    }
2102
    else
2103
    {
2104
      transaction.transferedSize += currentFile.size;
149✔
2105

2106
      emit syncProjectStatusChanged( projectFullName, transaction.transferedSize / transaction.totalSize );
149✔
2107
      transaction.pushQueue.removeFirst();
149✔
2108

2109
      if ( !transaction.pushQueue.isEmpty() )
149✔
2110
      {
2111
        MerginFile nextFile = transaction.pushQueue.first();
39✔
2112
        pushFile( projectFullName, transactionUUID, nextFile );
39✔
2113
      }
39✔
2114
      else
2115
      {
2116
        pushFinish( projectFullName, transactionUUID );
110✔
2117
      }
2118
    }
2119
  }
151✔
2120
  else
2121
  {
2122
    QString serverMsg = extractServerErrorMsg( r->readAll() );
2✔
2123
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) );
1✔
2124

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

2128
    transaction.replyPushFile->deleteLater();
1✔
2129
    transaction.replyPushFile = nullptr;
1✔
2130

2131
    finishProjectSync( projectFullName, false );
1✔
2132
  }
1✔
2133
}
152✔
2134

2135
void MerginApi::pullInfoReplyFinished()
100✔
2136
{
2137
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
100✔
2138
  Q_ASSERT( r );
100✔
2139

2140
  QString projectFullName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ) ).toString();
200✔
2141

2142
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
100✔
2143
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
100✔
2144
  Q_ASSERT( r == transaction.replyPullProjectInfo );
100✔
2145

2146
  if ( r->error() == QNetworkReply::NoError )
100✔
2147
  {
2148
    QByteArray data = r->readAll();
99✔
2149
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Downloaded project info." ) );
99✔
2150

2151
    transaction.replyPullProjectInfo->deleteLater();
99✔
2152
    transaction.replyPullProjectInfo = nullptr;
99✔
2153

2154
    prepareProjectPull( projectFullName, data );
99✔
2155
  }
99✔
2156
  else
2157
  {
2158
    QString serverMsg = extractServerErrorMsg( r->readAll() );
2✔
2159
    QString message = QStringLiteral( "Network API error: %1(): %2" ).arg( QStringLiteral( "projectInfo" ), r->errorString() );
3✔
2160
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) );
1✔
2161

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

2165
    transaction.replyPullProjectInfo->deleteLater();
1✔
2166
    transaction.replyPullProjectInfo = nullptr;
1✔
2167

2168
    finishProjectSync( projectFullName, false );
1✔
2169
  }
1✔
2170
}
100✔
2171

2172
void MerginApi::prepareProjectPull( const QString &projectFullName, const QByteArray &data )
125✔
2173
{
2174
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
125✔
2175
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
125✔
2176

2177
  MerginProjectMetadata serverProject = MerginProjectMetadata::fromJson( data );
125✔
2178

2179
  transaction.projectMetadata = data;
125✔
2180
  transaction.version = serverProject.version;
125✔
2181

2182
  LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName );
125✔
2183
  if ( projectInfo.isValid() )
125✔
2184
  {
2185
    transaction.projectDir = projectInfo.projectDir;
63✔
2186

2187
    // do not continue if we are already on the latest version
2188
    if ( projectInfo.localVersion != -1 && projectInfo.localVersion == serverProject.version )
63✔
2189
    {
2190
      emit projectAlreadyOnLatestVersion( projectFullName );
1✔
2191
      CoreUtils::log( QStringLiteral( "Pull %1" ).arg( projectFullName ), QStringLiteral( "Project is already on the latest version: %1" ).arg( serverProject.version ) );
2✔
2192

2193
      return finishProjectSync( projectFullName, false );
1✔
2194
    }
2195
  }
2196
  else
2197
  {
2198
    QString projectNamespace;
62✔
2199
    QString projectName;
62✔
2200
    extractProjectName( projectFullName, projectNamespace, projectName );
62✔
2201

2202
    // remove any leftover temp files that could be created from previous unsuccessful download
2203
    removeProjectsTempFolder( projectNamespace, projectName );
62✔
2204

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

2209
    // create file indicating first time download in progress
2210
    QString downloadInProgressFilePath = CoreUtils::downloadInProgressFilePath( transaction.projectDir );
62✔
2211
    createPathIfNotExists( downloadInProgressFilePath );
62✔
2212
    if ( !CoreUtils::createEmptyFile( downloadInProgressFilePath ) )
62✔
2213
      CoreUtils::log( QStringLiteral( "pull %1" ).arg( projectFullName ), "Unable to create temporary download in progress file" );
×
2214

2215
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "First time download - new directory: " ) + transaction.projectDir );
124✔
2216
  }
62✔
2217

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

2220
  if ( transaction.configAllowed )
124✔
2221
  {
2222
    prepareDownloadConfig( projectFullName );
112✔
2223
  }
2224
  else
2225
  {
2226
    startProjectPull( projectFullName );
12✔
2227
  }
2228
}
126✔
2229

2230
void MerginApi::startProjectPull( const QString &projectFullName )
124✔
2231
{
2232
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
124✔
2233
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
124✔
2234

2235
  QList<MerginFile> localFiles = getLocalProjectFiles( transaction.projectDir + "/" );
124✔
2236
  MerginProjectMetadata serverProject = MerginProjectMetadata::fromJson( transaction.projectMetadata );
124✔
2237
  MerginProjectMetadata oldServerProject = MerginProjectMetadata::fromCachedJson( transaction.projectDir + "/" + sMetadataFile );
248✔
2238
  MerginConfig oldTransactionConfig = MerginConfig::fromFile( transaction.projectDir + "/" + sMerginConfigFile );
248✔
2239

2240
  CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Updating from version %1 to version %2" )
372✔
2241
                  .arg( oldServerProject.version ).arg( serverProject.version ) );
248✔
2242

2243
  transaction.diff = compareProjectFiles(
124✔
2244
                       oldServerProject.files,
2245
                       serverProject.files,
2246
                       localFiles,
2247
                       transaction.projectDir,
124✔
2248
                       transaction.configAllowed,
124✔
2249
                       transaction.config,
124✔
2250
                       oldTransactionConfig );
124✔
2251

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

2254
  for ( QString filePath : transaction.diff.remoteAdded )
317✔
2255
  {
2256
    MerginFile file = serverProject.fileInfo( filePath );
193✔
2257
    QList<DownloadQueueItem> items = itemsForFileChunks( file, transaction.version );
193✔
2258
    transaction.pullTasks << PullTask( PullTask::Copy, filePath, items );
193✔
2259
    transaction.gpkgSchemaChanged = true;
193✔
2260
  }
193✔
2261

2262
  for ( QString filePath : transaction.diff.remoteUpdated )
138✔
2263
  {
2264
    MerginFile file = serverProject.fileInfo( filePath );
14✔
2265

2266
    // for diffable files - download and apply to the basefile (without rebase)
2267
    if ( isFileDiffable( filePath ) && file.pullCanUseDiff )
14✔
2268
    {
2269
      QList<DownloadQueueItem> items = itemsForFileDiffs( file );
6✔
2270
      transaction.pullTasks << PullTask( PullTask::ApplyDiff, filePath, items );
6✔
2271
    }
6✔
2272
    else
2273
    {
2274
      QList<DownloadQueueItem> items = itemsForFileChunks( file, transaction.version );
8✔
2275
      transaction.pullTasks << PullTask( PullTask::Copy, filePath, items );
8✔
2276
      transaction.gpkgSchemaChanged = true;
8✔
2277
    }
8✔
2278
  }
14✔
2279

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

2285
    // for diffable files - download and apply to the basefile (will also do rebase)
2286
    if ( isFileDiffable( filePath ) && file.pullCanUseDiff )
5✔
2287
    {
2288
      QList<DownloadQueueItem> items = itemsForFileDiffs( file );
3✔
2289
      transaction.pullTasks << PullTask( PullTask::ApplyDiff, filePath, items );
3✔
2290
    }
3✔
2291
    else
2292
    {
2293
      QList<DownloadQueueItem> items = itemsForFileChunks( file, transaction.version );
2✔
2294
      transaction.pullTasks << PullTask( PullTask::CopyConflict, filePath, items );
2✔
2295
      transaction.gpkgSchemaChanged = true;
2✔
2296
    }
2✔
2297
  }
5✔
2298

2299
  // also download files which were added both on the server and locally (the local version will be renamed as conflicting copy)
2300
  for ( QString filePath : transaction.diff.conflictRemoteAddedLocalAdded )
125✔
2301
  {
2302
    MerginFile file = serverProject.fileInfo( filePath );
1✔
2303
    QList<DownloadQueueItem> items = itemsForFileChunks( file, transaction.version );
1✔
2304
    transaction.pullTasks << PullTask( PullTask::CopyConflict, filePath, items );
1✔
2305
    transaction.gpkgSchemaChanged = true;
1✔
2306
  }
1✔
2307

2308
  // schedule removed files to be deleted
2309
  for ( QString filePath : transaction.diff.remoteDeleted )
127✔
2310
  {
2311
    transaction.pullTasks << PullTask( PullTask::Delete, filePath, QList<DownloadQueueItem>() );
3✔
2312
  }
3✔
2313

2314
  // prepare the download queue
2315
  for ( const PullTask &item : transaction.pullTasks )
340✔
2316
  {
2317
    transaction.downloadQueue << item.data;
216✔
2318
  }
2319

2320
  qint64 totalSize = 0;
124✔
2321
  for ( const DownloadQueueItem &item : transaction.downloadQueue )
277✔
2322
  {
2323
    totalSize += item.size;
153✔
2324
  }
2325
  transaction.totalSize = totalSize;
124✔
2326

2327
  CoreUtils::log( "pull " + projectFullName, QStringLiteral( "%1 update tasks, %2 items to download (total size %3 bytes)" )
372✔
2328
                  .arg( transaction.pullTasks.count() )
248✔
2329
                  .arg( transaction.downloadQueue.count() )
248✔
2330
                  .arg( transaction.totalSize ) );
248✔
2331

2332
  emit pullFilesStarted();
124✔
2333

2334
  if ( transaction.downloadQueue.isEmpty() )
124✔
2335
  {
2336
    finalizeProjectPull( projectFullName );
34✔
2337
  }
2338
  else
2339
  {
2340
    while ( transaction.replyPullItems.count() < 5 && !transaction.downloadQueue.isEmpty() )
243✔
2341
    {
2342
      downloadNextItem( projectFullName );
153✔
2343
    }
2344
  }
2345
}
124✔
2346

2347
void MerginApi::prepareDownloadConfig( const QString &projectFullName, bool downloaded )
151✔
2348
{
2349
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
151✔
2350
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
151✔
2351

2352
  MerginProjectMetadata newServerVersion = MerginProjectMetadata::fromJson( transaction.projectMetadata );
151✔
2353

2354
  const auto res = std::find_if( newServerVersion.files.begin(), newServerVersion.files.end(), []( const MerginFile & file )
151✔
2355
  {
2356
    return file.path == sMerginConfigFile;
531✔
2357
  } );
2358
  bool serverContainsConfig = res != newServerVersion.files.end();
151✔
2359

2360
  if ( serverContainsConfig )
151✔
2361
  {
2362
    if ( !downloaded )
78✔
2363
    {
2364
      // we should have server config but we do not have it yet
2365
      return requestServerConfig( projectFullName );
39✔
2366
    }
2367
  }
2368

2369
  MerginProjectMetadata oldServerVersion = MerginProjectMetadata::fromCachedJson( transaction.projectDir + "/" + sMetadataFile );
224✔
2370

2371
  const auto resOld = std::find_if( oldServerVersion.files.begin(), oldServerVersion.files.end(), []( const MerginFile & file )
112✔
2372
  {
2373
    return file.path == sMerginConfigFile;
205✔
2374
  } );
2375

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

2378
  if ( !transaction.config.isValid )
112✔
2379
  {
2380
    // if transaction is not valid (or missing), consider it as deleted
2381
    transaction.config.downloadMissingFiles = true;
74✔
2382
    CoreUtils::log( "MerginConfig", "No config detected" );
74✔
2383
  }
2384
  else if ( serverContainsConfig && previousVersionContainedConfig )
38✔
2385
  {
2386
    // config was there, check if there are changes
2387
    QString newChk = newServerVersion.fileInfo( sMerginConfigFile ).checksum;
29✔
2388
    QString oldChk = oldServerVersion.fileInfo( sMerginConfigFile ).checksum;
29✔
2389

2390
    if ( newChk == oldChk )
29✔
2391
    {
2392
      // config files are the same
2393
    }
2394
    else
2395
    {
2396
      // config was changed, but what changed?
2397
      MerginConfig oldConfig = MerginConfig::fromFile( transaction.projectDir + "/" + MerginApi::sMerginConfigFile );
16✔
2398

2399
      if ( oldConfig.selectiveSyncEnabled != transaction.config.selectiveSyncEnabled )
8✔
2400
      {
2401
        // selective sync was enabled/disabled
2402
        if ( transaction.config.selectiveSyncEnabled )
4✔
2403
        {
2404
          CoreUtils::log( "MerginConfig", "Selective sync has been enabled" );
2✔
2405
        }
2406
        else
2407
        {
2408
          CoreUtils::log( "MerginConfig", "Selective sync has been disabled, downloading missing files." );
2✔
2409
          transaction.config.downloadMissingFiles = true;
2✔
2410
        }
2411
      }
2412
      else if ( oldConfig.selectiveSyncDir != transaction.config.selectiveSyncDir )
4✔
2413
      {
2414
        CoreUtils::log( "MerginConfig", "Selective sync directory has changed, downloading missing files." );
4✔
2415
        transaction.config.downloadMissingFiles = true;
4✔
2416
      }
2417
      else
2418
      {
2419
        CoreUtils::log( "MerginConfig", "Unknown change in config file, continuing with latest version." );
×
2420
      }
2421
    }
8✔
2422
  }
29✔
2423
  else if ( serverContainsConfig )
9✔
2424
  {
2425
    CoreUtils::log( "MerginConfig", "Detected new config file." );
9✔
2426
  }
2427
  else if ( previousVersionContainedConfig ) // and current does not
×
2428
  {
2429
    CoreUtils::log( "MerginConfig", "Config file was removed, downloading missing files." );
×
2430
    transaction.config.downloadMissingFiles = true;
×
2431
  }
2432
  else // no config in last versions
2433
  {
2434
    // pull like without config
2435
    transaction.configAllowed = false;
×
2436
    transaction.config.isValid = false;
×
2437

2438
    // if it would be possible to add mergin-config locally, it needs to be checked here
2439
  }
2440

2441
  startProjectPull( projectFullName );
112✔
2442
}
151✔
2443

2444
void MerginApi::requestServerConfig( const QString &projectFullName )
39✔
2445
{
2446
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
39✔
2447
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
39✔
2448

2449
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/raw/" ) + projectFullName );
78✔
2450
  QUrlQuery query;
39✔
2451

2452
  query.addQueryItem( "file", sMerginConfigFile.toUtf8().toPercentEncoding() );
39✔
2453
  query.addQueryItem( "version", QStringLiteral( "v%1" ).arg( transaction.version ) );
39✔
2454
  url.setQuery( query );
39✔
2455

2456
  QNetworkRequest request = getDefaultRequest();
39✔
2457
  request.setUrl( url );
39✔
2458
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
39✔
2459

2460
  Q_ASSERT( !transaction.replyPullServerConfig );
39✔
2461
  transaction.replyPullServerConfig = mManager.get( request );
39✔
2462
  connect( transaction.replyPullServerConfig, &QNetworkReply::finished, this, &MerginApi::cacheServerConfig );
39✔
2463

2464
  CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Requesting mergin config: " ) + url.toString() );
78✔
2465
}
39✔
2466

2467
QList<DownloadQueueItem> MerginApi::itemsForFileChunks( const MerginFile &file, int version )
204✔
2468
{
2469
  QList<DownloadQueueItem> lst;
204✔
2470
  int from = 0;
204✔
2471
  while ( from < file.size )
347✔
2472
  {
2473
    int size = qMin( MerginApi::UPLOAD_CHUNK_SIZE, static_cast<int>( file.size ) - from );
143✔
2474
    lst << DownloadQueueItem( file.path, size, version, from, from + size - 1 );
143✔
2475
    from += size;
143✔
2476
  }
2477
  return lst;
204✔
2478
}
×
2479

2480
QList<DownloadQueueItem> MerginApi::itemsForFileDiffs( const MerginFile &file )
9✔
2481
{
2482
  QList<DownloadQueueItem> items;
9✔
2483
  // download diffs instead of full download of gpkg file from server
2484
  for ( const auto &d : file.pullDiffFiles )
19✔
2485
  {
2486
    items << DownloadQueueItem( file.path, d.second, d.first, -1, -1, true );
10✔
2487
  }
2488
  return items;
9✔
2489
}
×
2490

2491

2492
static MerginFile findFile( const QString &filePath, const QList<MerginFile> &files )
155✔
2493
{
2494
  for ( const MerginFile &merginFile : files )
417✔
2495
  {
2496
    if ( merginFile.path == filePath )
417✔
2497
      return merginFile;
155✔
2498
  }
2499
  CoreUtils::log( QStringLiteral( "MerginFile" ), QStringLiteral( "requested findFile() for non-existant file: %1" ).arg( filePath ) );
×
2500
  return MerginFile();
×
2501
}
2502

2503

2504
void MerginApi::pushInfoReplyFinished()
143✔
2505
{
2506
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
143✔
2507
  Q_ASSERT( r );
143✔
2508

2509
  QString projectFullName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ) ).toString();
286✔
2510

2511
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
143✔
2512
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
143✔
2513
  Q_ASSERT( r == transaction.replyPushProjectInfo );
143✔
2514

2515
  if ( r->error() == QNetworkReply::NoError )
143✔
2516
  {
2517
    QString url = r->url().toString();
142✔
2518
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Downloaded project info." ) );
142✔
2519
    QByteArray data = r->readAll();
142✔
2520

2521
    transaction.replyPushProjectInfo->deleteLater();
142✔
2522
    transaction.replyPushProjectInfo = nullptr;
142✔
2523

2524
    LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName );
142✔
2525
    transaction.projectDir = projectInfo.projectDir;
142✔
2526
    Q_ASSERT( !transaction.projectDir.isEmpty() );
142✔
2527

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

2531
    // now let's figure a key question: are we on the most recent version of the project
2532
    // if we're about to do upload? because if not, we need to do pull first
2533
    if ( projectInfo.isValid() && projectInfo.localVersion != -1 && projectInfo.localVersion < serverProject.version )
142✔
2534
    {
2535
      CoreUtils::log( "push " + projectFullName, QStringLiteral( "Need pull first: local version %1 | server version %2" )
78✔
2536
                      .arg( projectInfo.localVersion ).arg( serverProject.version ) );
52✔
2537
      transaction.pullBeforePush = true;
26✔
2538
      prepareProjectPull( projectFullName, data );
26✔
2539
      return;
26✔
2540
    }
2541

2542
    QList<MerginFile> localFiles = getLocalProjectFiles( transaction.projectDir + "/" );
116✔
2543
    MerginProjectMetadata oldServerProject = MerginProjectMetadata::fromCachedJson( transaction.projectDir + "/" + sMetadataFile );
232✔
2544

2545
    // Cache mergin-config, since we are on the most recent version, it is sufficient to just read the local version
2546
    if ( transaction.configAllowed )
116✔
2547
    {
2548
      transaction.config = MerginConfig::fromFile( transaction.projectDir + "/" + MerginApi::sMerginConfigFile );
107✔
2549
    }
2550

2551
    transaction.diff = compareProjectFiles(
232✔
2552
                         oldServerProject.files,
2553
                         serverProject.files,
2554
                         localFiles,
2555
                         transaction.projectDir,
116✔
2556
                         transaction.configAllowed,
116✔
2557
                         transaction.config
116✔
2558
                       );
116✔
2559

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

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

2564
    QList<MerginFile> filesToUpload;
116✔
2565
    QList<MerginFile> addedMerginFiles, updatedMerginFiles, deletedMerginFiles;
116✔
2566
    QList<MerginFile> diffFiles;
116✔
2567
    for ( QString filePath : transaction.diff.localAdded )
248✔
2568
    {
2569
      MerginFile merginFile = findFile( filePath, localFiles );
132✔
2570
      merginFile.chunks = generateChunkIdsForSize( merginFile.size );
132✔
2571
      addedMerginFiles.append( merginFile );
132✔
2572
    }
132✔
2573

2574
    for ( QString filePath : transaction.diff.localUpdated )
135✔
2575
    {
2576
      MerginFile merginFile = findFile( filePath, localFiles );
19✔
2577
      merginFile.chunks = generateChunkIdsForSize( merginFile.size );
19✔
2578

2579
      if ( MerginApi::isFileDiffable( filePath ) )
19✔
2580
      {
2581
        // try to create a diff
2582
        QString diffName;
12✔
2583
        int geodiffRes = GeodiffUtils::createChangeset( transaction.projectDir, filePath, diffName );
12✔
2584
        QString diffPath = transaction.projectDir + "/.mergin/" + diffName;
12✔
2585
        QString basePath = transaction.projectDir + "/.mergin/" + filePath;
12✔
2586

2587
        if ( geodiffRes == GEODIFF_SUCCESS )
12✔
2588
        {
2589
          QByteArray checksumDiff = CoreUtils::calculateChecksum( diffPath );
12✔
2590

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

2595
          merginFile.diffName = diffName;
12✔
2596
          merginFile.diffChecksum = QString::fromLatin1( checksumDiff.data(), checksumDiff.size() );
12✔
2597
          merginFile.diffSize = QFileInfo( diffPath ).size();
12✔
2598
          merginFile.chunks = generateChunkIdsForSize( merginFile.diffSize );
12✔
2599
          merginFile.diffBaseChecksum = QString::fromLatin1( checksumBase.data(), checksumBase.size() );
12✔
2600

2601
          diffFiles.append( merginFile );
12✔
2602

2603
          CoreUtils::log( "push " + projectFullName, QString( "Geodiff create changeset on %1 successful: total size %2 bytes" ).arg( filePath ).arg( merginFile.diffSize ) );
12✔
2604
        }
12✔
2605
        else
2606
        {
2607
          // TODO: remove the diff file (if exists)
2608
          CoreUtils::log( "push " + projectFullName, QString( "Geodiff create changeset on %1 FAILED with error %2 (will do full upload)" ).arg( filePath ).arg( geodiffRes ) );
×
2609
        }
2610
      }
12✔
2611

2612
      updatedMerginFiles.append( merginFile );
19✔
2613
    }
19✔
2614

2615
    for ( QString filePath : transaction.diff.localDeleted )
120✔
2616
    {
2617
      MerginFile merginFile = findFile( filePath, serverProject.files );
4✔
2618
      deletedMerginFiles.append( merginFile );
4✔
2619
    }
4✔
2620

2621
    if ( addedMerginFiles.isEmpty() && updatedMerginFiles.isEmpty() && deletedMerginFiles.isEmpty() )
116✔
2622
    {
2623
      // if nothing has changed, there is no point to even start upload transaction
2624
      transaction.projectMetadata = data;
1✔
2625
      transaction.version = MerginProjectMetadata::fromJson( data ).version;
1✔
2626

2627
      finishProjectSync( projectFullName, true );
1✔
2628
      return;
1✔
2629
    }
2630

2631
    QJsonArray added = prepareUploadChangesJSON( addedMerginFiles );
115✔
2632
    filesToUpload.append( addedMerginFiles );
115✔
2633

2634
    QJsonArray modified = prepareUploadChangesJSON( updatedMerginFiles );
115✔
2635
    filesToUpload.append( updatedMerginFiles );
115✔
2636

2637
    QJsonArray removed = prepareUploadChangesJSON( deletedMerginFiles );
115✔
2638
    // removed not in filesToUpload
2639

2640
    QJsonObject changes;
115✔
2641
    changes.insert( "added", added );
115✔
2642
    changes.insert( "removed", removed );
115✔
2643
    changes.insert( "updated", modified );
115✔
2644
    changes.insert( "renamed", QJsonArray() );
115✔
2645

2646
    qint64 totalSize = 0;
115✔
2647
    for ( MerginFile file : filesToUpload )
266✔
2648
    {
2649
      if ( !file.diffName.isEmpty() )
151✔
2650
        totalSize += file.diffSize;
12✔
2651
      else
2652
        totalSize += file.size;
139✔
2653
    }
151✔
2654

2655
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "%1 items to upload (total size %2 bytes)" )
345✔
2656
                    .arg( filesToUpload.count() ).arg( totalSize ) );
230✔
2657

2658
    transaction.totalSize = totalSize;
115✔
2659
    transaction.pushQueue = filesToUpload;
115✔
2660
    transaction.pushDiffFiles = diffFiles;
115✔
2661

2662
    QJsonObject json;
115✔
2663
    json.insert( QStringLiteral( "changes" ), changes );
230✔
2664
    json.insert( QStringLiteral( "version" ), QString( "v%1" ).arg( serverProject.version ) );
230✔
2665
    QJsonDocument jsonDoc;
115✔
2666
    jsonDoc.setObject( json );
115✔
2667

2668
    pushStart( projectFullName, jsonDoc.toJson( QJsonDocument::Compact ) );
115✔
2669
  }
230✔
2670
  else
2671
  {
2672
    QString serverMsg = extractServerErrorMsg( r->readAll() );
2✔
2673
    QString message = QStringLiteral( "Network API error: %1(): %2" ).arg( QStringLiteral( "projectInfo" ), r->errorString() );
3✔
2674
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) );
1✔
2675

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

2679
    transaction.replyPushProjectInfo->deleteLater();
1✔
2680
    transaction.replyPushProjectInfo = nullptr;
1✔
2681

2682
    finishProjectSync( projectFullName, false );
1✔
2683
  }
1✔
2684
}
143✔
2685

2686
void MerginApi::pushFinishReplyFinished()
110✔
2687
{
2688
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
110✔
2689
  Q_ASSERT( r );
110✔
2690

2691
  QString projectFullName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ) ).toString();
220✔
2692

2693
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
110✔
2694
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
110✔
2695
  Q_ASSERT( r == transaction.replyPushFinish );
110✔
2696

2697
  if ( r->error() == QNetworkReply::NoError )
110✔
2698
  {
2699
    Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
110✔
2700
    QByteArray data = r->readAll();
110✔
2701
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Transaction finish accepted" ) );
110✔
2702

2703
    transaction.replyPushFinish->deleteLater();
110✔
2704
    transaction.replyPushFinish = nullptr;
110✔
2705

2706
    transaction.projectMetadata = data;
110✔
2707
    transaction.version = MerginProjectMetadata::fromJson( data ).version;
110✔
2708

2709
    //  a new diffable files suppose to have their basefile copies in .mergin
2710
    for ( QString filePath : transaction.diff.localAdded )
240✔
2711
    {
2712
      if ( MerginApi::isFileDiffable( filePath ) )
130✔
2713
      {
2714
        QString basefile = transaction.projectDir + "/.mergin/" + filePath;
11✔
2715
        createPathIfNotExists( basefile );
11✔
2716

2717
        QString sourcePath = transaction.projectDir + "/" + filePath;
11✔
2718
        if ( !QFile::copy( sourcePath, basefile ) )
11✔
2719
        {
2720
          CoreUtils::log( "push " + projectFullName, "failed to copy new basefile for: " + filePath );
×
2721
        }
2722
      }
11✔
2723
    }
130✔
2724

2725
    // clean up diff-related files
2726
    const auto diffFiles = transaction.pushDiffFiles;
110✔
2727
    for ( const MerginFile &merginFile : diffFiles )
122✔
2728
    {
2729
      QString diffPath = transaction.projectDir + "/.mergin/" + merginFile.diffName;
12✔
2730

2731
      // update basefile (unmodified file that should be equivalent to the server)
2732
      QString basePath = transaction.projectDir + "/.mergin/" + merginFile.path;
12✔
2733
      bool res = GeodiffUtils::applyChangeset( basePath, diffPath );
12✔
2734
      if ( res )
12✔
2735
      {
2736
        CoreUtils::log( "push " + projectFullName, QString( "Applied %1 to base file of %2" ).arg( merginFile.diffName, merginFile.path ) );
12✔
2737
      }
2738
      else
2739
      {
2740
        CoreUtils::log( "push " + projectFullName, QString( "Failed to apply changeset %1 to basefile %2 - error %3" ).arg( diffPath ).arg( basePath ).arg( res ) );
×
2741
      }
2742

2743
      // remove temporary diff files
2744
      if ( !QFile::remove( diffPath ) )
12✔
2745
        CoreUtils::log( "push " + projectFullName, "Failed to remove diff: " + diffPath );
×
2746
    }
12✔
2747

2748
    finishProjectSync( projectFullName, true );
110✔
2749
  }
110✔
2750
  else
2751
  {
2752
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
2753
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "pushFinish" ), r->errorString(), serverMsg );
×
2754
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) );
×
2755

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

2759
    // remove temporary diff files
2760
    const auto diffFiles = transaction.pushDiffFiles;
×
2761
    for ( const MerginFile &merginFile : diffFiles )
×
2762
    {
2763
      QString diffPath = transaction.projectDir + "/.mergin/" + merginFile.diffName;
×
2764
      if ( !QFile::remove( diffPath ) )
×
2765
        CoreUtils::log( "push " + projectFullName, "Failed to remove diff: " + diffPath );
×
2766
    }
×
2767

2768
    transaction.replyPushFinish->deleteLater();
×
2769
    transaction.replyPushFinish = nullptr;
×
2770

2771
    finishProjectSync( projectFullName, false );
×
2772
  }
×
2773
}
110✔
2774

2775
void MerginApi::pushCancelReplyFinished()
1✔
2776
{
2777
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
1✔
2778
  Q_ASSERT( r );
1✔
2779

2780
  QString projectFullName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ) ).toString();
2✔
2781

2782
  if ( r->error() == QNetworkReply::NoError )
1✔
2783
  {
2784
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Transaction canceled" ) );
1✔
2785
  }
2786
  else
2787
  {
2788
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
2789
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "uploadCancel" ), r->errorString(), serverMsg );
×
2790
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) );
×
2791
  }
×
2792

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

2795
  r->deleteLater();
1✔
2796
}
1✔
2797

2798
void MerginApi::getUserInfoFinished()
19✔
2799
{
2800
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
19✔
2801
  Q_ASSERT( r );
19✔
2802

2803
  if ( r->error() == QNetworkReply::NoError )
19✔
2804
  {
2805
    CoreUtils::log( "user info", QStringLiteral( "Success" ) );
18✔
2806
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
18✔
2807
    if ( doc.isObject() )
18✔
2808
    {
2809
      QJsonObject docObj = doc.object();
18✔
2810
      mUserInfo->setFromJson( docObj );
18✔
2811
      if ( mServerType == MerginServerType::OLD )
18✔
2812
      {
2813
        mWorkspaceInfo->setFromJson( docObj );
×
2814
      }
2815
    }
18✔
2816
  }
18✔
2817
  else
2818
  {
2819
    QString serverMsg = extractServerErrorMsg( r->readAll() );
2✔
2820
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "getUserInfo" ), r->errorString(), serverMsg );
3✔
2821
    CoreUtils::log( "user info", QStringLiteral( "FAILED - %1" ).arg( message ) );
1✔
2822
    mUserInfo->clear();
1✔
2823
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: getUserInfo" ) );
2✔
2824
  }
1✔
2825

2826
  emit userInfoReplyFinished();
19✔
2827

2828
  r->deleteLater();
19✔
2829
}
19✔
2830

2831
void MerginApi::getWorkspaceInfoReplyFinished()
25✔
2832
{
2833
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
25✔
2834
  Q_ASSERT( r );
25✔
2835

2836
  if ( r->error() == QNetworkReply::NoError )
25✔
2837
  {
2838
    CoreUtils::log( "workspace info", QStringLiteral( "Success" ) );
25✔
2839
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
25✔
2840
    if ( doc.isObject() )
25✔
2841
    {
2842
      QJsonObject docObj = doc.object();
25✔
2843
      mWorkspaceInfo->setFromJson( docObj );
25✔
2844

2845
      emit getWorkspaceInfoFinished();
25✔
2846
    }
25✔
2847
  }
25✔
2848
  else
2849
  {
2850
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
2851
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "getWorkspaceInfo" ), r->errorString(), serverMsg );
×
2852
    CoreUtils::log( "workspace info", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
2853
    mWorkspaceInfo->clear();
×
2854
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: getWorkspaceInfo" ) );
×
2855
  }
×
2856

2857
  r->deleteLater();
25✔
2858
}
25✔
2859

2860
bool MerginApi::hasLocalChanges(
328✔
2861
  const QList<MerginFile> &oldServerFiles,
2862
  const QList<MerginFile> &localFiles,
2863
  const QString &projectDir
2864
)
2865
{
2866
  if ( localFiles.count() != oldServerFiles.count() )
328✔
2867
  {
2868
    return true;
24✔
2869
  }
2870

2871
  QHash<QString, MerginFile> oldServerFilesMap;
304✔
2872

2873
  for ( const MerginFile &file : oldServerFiles )
970✔
2874
  {
2875
    oldServerFilesMap.insert( file.path, file );
666✔
2876
  }
2877

2878
  for ( const MerginFile &localFile : localFiles )
957✔
2879
  {
2880
    QString filePath = localFile.path;
666✔
2881
    bool hasOldServer = oldServerFilesMap.contains( localFile.path );
666✔
2882

2883
    if ( !hasOldServer )
666✔
2884
    {
2885
      // L-A
2886
      return true;
×
2887
    }
2888
    else
2889
    {
2890
      const QString chkOld = oldServerFilesMap.value( localFile.path ).checksum;
666✔
2891
      const QString chkLocal = localFile.checksum;
666✔
2892

2893
      if ( chkOld != chkLocal )
666✔
2894
      {
2895
        if ( isFileDiffable( filePath ) )
67✔
2896
        {
2897
          // we need to do a diff here to figure out whether the file is actually changed or not
2898
          // because the real content may be the same although the checksums do not match
2899
          // e.g. when GPKG is opened, its header is updated and therefore lastModified timestamp/checksum is updated as well.
2900
          if ( GeodiffUtils::hasPendingChanges( projectDir, filePath ) )
66✔
2901
          {
2902
            // L-U
2903
            return true;
12✔
2904
          }
2905
        }
2906
        else
2907
        {
2908
          // L-U
2909
          return true;
1✔
2910
        }
2911
      }
2912
    }
679✔
2913
  }
666✔
2914

2915
  // We know that the number of local files and old server is the same
2916
  // And also that all local files has old file counterpart
2917
  // So it is not possible that there is deleted local file at this point.
2918
  return false;
291✔
2919
}
304✔
2920

2921
ProjectDiff MerginApi::compareProjectFiles(
257✔
2922
  const QList<MerginFile> &oldServerFiles,
2923
  const QList<MerginFile> &newServerFiles,
2924
  const QList<MerginFile> &localFiles,
2925
  const QString &projectDir,
2926
  bool allowConfig,
2927
  const MerginConfig &config,
2928
  const MerginConfig &lastSyncConfig
2929
)
2930
{
2931
  ProjectDiff diff;
257✔
2932
  QHash<QString, MerginFile> oldServerFilesMap, newServerFilesMap;
257✔
2933

2934
  for ( MerginFile file : newServerFiles )
1,384✔
2935
  {
2936
    newServerFilesMap.insert( file.path, file );
1,127✔
2937
  }
1,127✔
2938
  for ( MerginFile file : oldServerFiles )
1,178✔
2939
  {
2940
    oldServerFilesMap.insert( file.path, file );
921✔
2941
  }
921✔
2942

2943
  for ( MerginFile localFile : localFiles )
1,164✔
2944
  {
2945
    QString filePath = localFile.path;
907✔
2946
    bool hasOldServer = oldServerFilesMap.contains( localFile.path );
907✔
2947
    bool hasNewServer = newServerFilesMap.contains( localFile.path );
907✔
2948
    QString chkOld = oldServerFilesMap.value( localFile.path ).checksum;
907✔
2949
    QString chkNew = newServerFilesMap.value( localFile.path ).checksum;
907✔
2950
    QString chkLocal = localFile.checksum;
907✔
2951

2952
    if ( !hasOldServer && !hasNewServer )
907✔
2953
    {
2954
      // L-A
2955
      diff.localAdded << filePath;
159✔
2956
    }
2957
    else if ( hasOldServer && !hasNewServer )
748✔
2958
    {
2959
      if ( chkOld == chkLocal )
4✔
2960
      {
2961
        // R-D
2962
        diff.remoteDeleted << filePath;
3✔
2963
      }
2964
      else
2965
      {
2966
        // C/R-D/L-U
2967
        diff.conflictRemoteDeletedLocalUpdated << filePath;
1✔
2968
      }
2969
    }
2970
    else if ( !hasOldServer && hasNewServer )
744✔
2971
    {
2972
      if ( chkNew != chkLocal )
1✔
2973
      {
2974
        // C/R-A/L-A
2975
        diff.conflictRemoteAddedLocalAdded << filePath;
1✔
2976
      }
2977
      else
2978
      {
2979
        // R-A/L-A
2980
        // TODO: need to do anything?
2981
      }
2982
    }
2983
    else if ( hasOldServer && hasNewServer )
743✔
2984
    {
2985
      // file has already existed
2986
      if ( chkOld == chkNew )
743✔
2987
      {
2988
        if ( chkNew != chkLocal )
724✔
2989
        {
2990
          // L-U
2991
          if ( isFileDiffable( filePath ) )
29✔
2992
          {
2993
            // we need to do a diff here to figure out whether the file is actually changed or not
2994
            // because the real content may be the same although the checksums do not match
2995
            if ( GeodiffUtils::hasPendingChanges( projectDir, filePath ) )
22✔
2996
              diff.localUpdated << filePath;
17✔
2997
          }
2998
          else
2999
            diff.localUpdated << filePath;
7✔
3000
        }
3001
        else
3002
        {
3003
          // no change :-)
3004
        }
3005
      }
3006
      else   // v1 != v2
3007
      {
3008
        if ( chkNew != chkLocal && chkOld != chkLocal )
19✔
3009
        {
3010
          // C/R-U/L-U
3011
          if ( isFileDiffable( filePath ) )
7✔
3012
          {
3013
            // we need to do a diff here to figure out whether the file is actually changed or not
3014
            // because the real content may be the same although the checksums do not match
3015
            if ( GeodiffUtils::hasPendingChanges( projectDir, filePath ) )
5✔
3016
              diff.conflictRemoteUpdatedLocalUpdated << filePath;
3✔
3017
            else
3018
              diff.remoteUpdated << filePath;
2✔
3019
          }
3020
          else
3021
            diff.conflictRemoteUpdatedLocalUpdated << filePath;
2✔
3022
        }
3023
        else if ( chkNew != chkLocal )  // && old == local
12✔
3024
        {
3025
          // R-U
3026
          diff.remoteUpdated << filePath;
12✔
3027
        }
3028
        else if ( chkOld != chkLocal )  // && new == local
×
3029
        {
3030
          // R-U/L-U
3031
          // TODO: need to do anything?
3032
        }
3033
        else
3034
          Q_ASSERT( false );   // impossible - should be handled already
×
3035
      }
3036
    }
3037

3038
    if ( hasOldServer )
907✔
3039
      oldServerFilesMap.remove( filePath );
747✔
3040
    if ( hasNewServer )
907✔
3041
      newServerFilesMap.remove( filePath );
744✔
3042
  }
907✔
3043

3044
  // go through files listed on the server, but not available locally
3045
  for ( MerginFile file : newServerFilesMap )
640✔
3046
  {
3047
    bool hasOldServer = oldServerFilesMap.contains( file.path );
383✔
3048

3049
    if ( hasOldServer )
383✔
3050
    {
3051
      if ( oldServerFilesMap.value( file.path ).checksum == file.checksum )
174✔
3052
      {
3053
        // L-D
3054
        if ( allowConfig )
174✔
3055
        {
3056
          bool shouldBeExcludedFromSync = MerginApi::excludeFromSync( file.path, config );
173✔
3057
          if ( shouldBeExcludedFromSync )
173✔
3058
          {
3059
            continue;
151✔
3060
          }
3061

3062
          // check if we should download missing files that were previously ignored (e.g. selective sync has been disabled)
3063
          bool previouslyIgnoredButShouldDownload = \
3064
              config.downloadMissingFiles &&
41✔
3065
              lastSyncConfig.isValid &&
41✔
3066
              MerginApi::excludeFromSync( file.path, lastSyncConfig );
19✔
3067

3068
          if ( previouslyIgnoredButShouldDownload )
22✔
3069
          {
3070
            diff.remoteAdded << file.path;
19✔
3071
            continue;
19✔
3072
          }
3073
        }
3074
        diff.localDeleted << file.path;
4✔
3075
      }
3076
      else
3077
      {
3078
        // C/R-U/L-D
3079
        diff.conflictRemoteUpdatedLocalDeleted << file.path;
×
3080
      }
3081
    }
3082
    else
3083
    {
3084
      // R-A
3085
      if ( allowConfig )
209✔
3086
      {
3087
        if ( MerginApi::excludeFromSync( file.path, config ) )
167✔
3088
        {
3089
          continue;
35✔
3090
        }
3091
      }
3092
      diff.remoteAdded << file.path;
174✔
3093
    }
3094

3095
    if ( hasOldServer )
178✔
3096
      oldServerFilesMap.remove( file.path );
4✔
3097
  }
383✔
3098

3099
  /*
3100
  for ( MerginFile file : oldServerFilesMap )
3101
  {
3102
    // R-D/L-D
3103
    // TODO: need to do anything?
3104
  }
3105
  */
3106

3107
  return diff;
514✔
3108
}
257✔
3109

3110
MerginProject MerginApi::parseProjectMetadata( const QJsonObject &proj )
286✔
3111
{
3112
  MerginProject project;
286✔
3113

3114
  if ( proj.isEmpty() )
286✔
3115
  {
3116
    return project;
×
3117
  }
3118

3119
  if ( proj.contains( QStringLiteral( "error" ) ) )
286✔
3120
  {
3121
    // handle project error (user might be logged out / do not have write rights / project is on different server / project is orphaned)
3122
    project.remoteError = QString::number( proj.value( QStringLiteral( "error" ) ).toInt( 0 ) ); // error code
×
3123
    return project;
×
3124
  }
3125

3126
  project.projectName = proj.value( QStringLiteral( "name" ) ).toString();
286✔
3127
  project.projectNamespace = proj.value( QStringLiteral( "namespace" ) ).toString();
286✔
3128

3129
  QString versionStr = proj.value( QStringLiteral( "version" ) ).toString();
572✔
3130
  if ( versionStr.isEmpty() )
286✔
3131
  {
3132
    project.serverVersion = 0;
×
3133
  }
3134
  else if ( versionStr.startsWith( "v" ) ) // cut off 'v' part from v123
286✔
3135
  {
3136
    versionStr = versionStr.mid( 1 );
286✔
3137
    project.serverVersion = versionStr.toInt();
286✔
3138
  }
3139

3140
  QDateTime updated = QDateTime::fromString( proj.value( QStringLiteral( "updated" ) ).toString(), Qt::ISODateWithMs ).toUTC();
572✔
3141
  if ( !updated.isValid() )
286✔
3142
  {
3143
    project.serverUpdated = QDateTime::fromString( proj.value( QStringLiteral( "created" ) ).toString(), Qt::ISODateWithMs ).toUTC();
×
3144
  }
3145
  else
3146
  {
3147
    project.serverUpdated = updated;
286✔
3148
  }
3149
  return project;
286✔
3150
}
286✔
3151

3152

3153
MerginProjectsList MerginApi::parseProjectsFromJson( const QJsonDocument &doc )
29✔
3154
{
3155
  if ( !doc.isObject() )
29✔
3156
    return MerginProjectsList();
×
3157

3158
  QJsonObject object = doc.object();
29✔
3159
  MerginProjectsList result;
29✔
3160

3161
  if ( object.contains( "projects" ) && object.value( "projects" ).isArray() ) // listProjects API
29✔
3162
  {
3163
    QJsonArray vArray = object.value( "projects" ).toArray();
44✔
3164

3165
    for ( auto it = vArray.constBegin(); it != vArray.constEnd(); ++it )
195✔
3166
    {
3167
      result << parseProjectMetadata( it->toObject() );
173✔
3168
    }
3169
  }
22✔
3170
  else if ( !object.isEmpty() ) // listProjectsbyName API returns projects as separate objects not in array
7✔
3171
  {
3172
    for ( auto it = object.begin(); it != object.end(); ++it )
120✔
3173
    {
3174
      MerginProject project = parseProjectMetadata( it->toObject() );
113✔
3175
      if ( !project.remoteError.isEmpty() )
113✔
3176
      {
3177
        // add project namespace/name from object name in case of error
3178
        MerginApi::extractProjectName( it.key(), project.projectNamespace, project.projectName );
×
3179
      }
3180
      result << project;
113✔
3181
    }
113✔
3182
  }
3183
  return result;
29✔
3184
}
29✔
3185

3186
void MerginApi::refreshAuthToken()
7✔
3187
{
3188
  if ( !mUserAuth->hasAuthData() ||
14✔
3189
       mUserAuth->authToken().isEmpty() )
14✔
3190
  {
3191
    CoreUtils::log( QStringLiteral( "Auth" ), QStringLiteral( "Can not refresh token, missing credentials" ) );
×
3192
    return;
×
3193
  }
3194

3195
  if ( mUserAuth->tokenExpiration() < QDateTime::currentDateTimeUtc() )
7✔
3196
  {
3197
    CoreUtils::log( QStringLiteral( "Auth" ), QStringLiteral( "Token has expired, requesting new one" ) );
×
3198
    authorize( mUserAuth->username(), mUserAuth->password() );
×
3199
    mAuthLoopEvent.exec();
×
3200
  }
3201
}
3202

3203
QStringList MerginApi::generateChunkIdsForSize( qint64 fileSize )
163✔
3204
{
3205
  qreal rawNoOfChunks = qreal( fileSize ) / UPLOAD_CHUNK_SIZE;
163✔
3206
  int noOfChunks = qCeil( rawNoOfChunks );
163✔
3207

3208
  // edge case when file is empty, filesize equals zero
3209
  // manually set one chunk so that file will be synced
3210
  if ( fileSize <= 0 )
163✔
3211
    noOfChunks = 1;
45✔
3212

3213
  QStringList chunks;
163✔
3214
  for ( int i = 0; i < noOfChunks; i++ )
328✔
3215
  {
3216
    QString chunkID = CoreUtils::uuidWithoutBraces( QUuid::createUuid() );
165✔
3217
    chunks.append( chunkID );
165✔
3218
  }
165✔
3219
  return chunks;
163✔
3220
}
×
3221

3222
QJsonArray MerginApi::prepareUploadChangesJSON( const QList<MerginFile> &files )
345✔
3223
{
3224
  QJsonArray jsonArray;
345✔
3225

3226
  for ( MerginFile file : files )
500✔
3227
  {
3228
    QJsonObject fileObject;
155✔
3229
    fileObject.insert( "path", file.path );
155✔
3230

3231
    fileObject.insert( "size", file.size );
155✔
3232
    fileObject.insert( "mtime", file.mtime.toString( Qt::ISODateWithMs ) );
155✔
3233

3234
    if ( !file.diffName.isEmpty() )
155✔
3235
    {
3236
      // doing diff-based upload
3237
      QJsonObject diffObject;
12✔
3238
      diffObject.insert( "path", file.diffName );
12✔
3239
      diffObject.insert( "checksum", file.diffChecksum );
12✔
3240
      diffObject.insert( "size", file.diffSize );
12✔
3241

3242
      fileObject.insert( "diff", diffObject );
12✔
3243
      fileObject.insert( "checksum", file.diffBaseChecksum );
12✔
3244
    }
12✔
3245
    else
3246
    {
3247
      fileObject.insert( "checksum", file.checksum );
143✔
3248
    }
3249

3250
    QJsonArray chunksJson;
155✔
3251
    for ( QString id : file.chunks )
308✔
3252
    {
3253
      chunksJson.append( id );
153✔
3254
    }
153✔
3255
    fileObject.insert( "chunks", chunksJson );
155✔
3256
    jsonArray.append( fileObject );
155✔
3257
  }
155✔
3258
  return jsonArray;
345✔
3259
}
×
3260

3261
void MerginApi::finishProjectSync( const QString &projectFullName, bool syncSuccessful )
243✔
3262
{
3263
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
243✔
3264
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
243✔
3265

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

3268
  if ( syncSuccessful )
243✔
3269
  {
3270
    // update the local metadata file
3271
    writeData( transaction.projectMetadata, transaction.projectDir + "/" + MerginApi::sMetadataFile );
238✔
3272

3273
    // update info of local projects
3274
    mLocalProjects.updateLocalVersion( transaction.projectDir, transaction.version );
238✔
3275

3276
    CoreUtils::log( "sync " + projectFullName, QStringLiteral( "### Finished ###  New project version: %1\n" ).arg( transaction.version ) );
238✔
3277
  }
3278
  else
3279
  {
3280
    CoreUtils::log( "sync " + projectFullName, QStringLiteral( "### FAILED ###\n" ) );
5✔
3281
  }
3282

3283
  bool pullBeforePush = transaction.pullBeforePush;
243✔
3284
  QString projectDir = transaction.projectDir;  // keep it before the transaction gets removed
243✔
3285
  ProjectDiff diff = transaction.diff;
243✔
3286
  int newVersion = syncSuccessful ? transaction.version : -1;
243✔
3287

3288
  if ( transaction.gpkgSchemaChanged || projectFileHasBeenUpdated( diff ) )
243✔
3289
  {
3290
    emit projectReloadNeededAfterSync( projectFullName );
96✔
3291
  }
3292

3293
  mTransactionalStatus.remove( projectFullName );
243✔
3294

3295
  if ( pullBeforePush )
243✔
3296
  {
3297
    CoreUtils::log( "sync " + projectFullName, QStringLiteral( "Continue with push after pull" ) );
26✔
3298
    // we're done only with the download part before the actual upload - so let's continue with upload
3299
    QString projectNamespace, projectName;
26✔
3300
    extractProjectName( projectFullName, projectNamespace, projectName );
26✔
3301
    pushProject( projectNamespace, projectName );
26✔
3302
  }
26✔
3303
  else
3304
  {
3305
    emit syncProjectFinished( projectFullName, syncSuccessful, newVersion );
217✔
3306

3307
    if ( syncSuccessful )
217✔
3308
    {
3309
      emit projectDataChanged( projectFullName );
212✔
3310
    }
3311
  }
3312
}
243✔
3313

3314
bool MerginApi::writeData( const QByteArray &data, const QString &path )
238✔
3315
{
3316
  QFile file( path );
238✔
3317
  createPathIfNotExists( path );
238✔
3318
  if ( !file.open( QIODevice::WriteOnly ) )
238✔
3319
  {
3320
    return false;
×
3321
  }
3322

3323
  file.write( data );
238✔
3324
  file.close();
238✔
3325

3326
  return true;
238✔
3327
}
238✔
3328

3329

3330
void MerginApi::createPathIfNotExists( const QString &filePath )
706✔
3331
{
3332
  QDir dir;
706✔
3333
  if ( !dir.exists( mDataDir ) )
706✔
3334
    dir.mkpath( mDataDir );
×
3335

3336
  QFileInfo newFile( filePath );
706✔
3337
  if ( !newFile.absoluteDir().exists() )
706✔
3338
  {
3339
    if ( !dir.mkpath( newFile.absolutePath() ) )
160✔
3340
    {
3341
      CoreUtils::log( "create path", QString( "Creating a folder failed for path: %1" ).arg( filePath ) );
×
3342
    }
3343
  }
3344
}
706✔
3345

3346
bool MerginApi::isInIgnore( const QFileInfo &info )
1,659✔
3347
{
3348
  return sIgnoreExtensions.contains( info.suffix() ) || sIgnoreFiles.contains( info.fileName() ) || info.filePath().contains( sMetadataFolder + "/" );
1,659✔
3349
}
3350

3351
bool MerginApi::excludeFromSync( const QString &filePath, const MerginConfig &config )
373✔
3352
{
3353
  if ( config.isValid && config.selectiveSyncEnabled )
373✔
3354
  {
3355
    QFileInfo info( filePath );
243✔
3356

3357
    bool isExcludedFormat = sIgnoreImageExtensions.contains( info.suffix().toLower() );
243✔
3358

3359
    if ( !isExcludedFormat )
243✔
3360
      return false;
20✔
3361

3362
    if ( config.selectiveSyncDir.isEmpty() )
223✔
3363
    {
3364
      return true; // we are ignoring photos in the entire project
96✔
3365
    }
3366
    else if ( filePath.startsWith( config.selectiveSyncDir ) )
127✔
3367
    {
3368
      return true; // we are ignoring photo in subfolder
119✔
3369
    }
3370
  }
243✔
3371
  return false;
138✔
3372
}
3373

3374
QSet<QString> MerginApi::listFiles( const QString &path )
585✔
3375
{
3376
  QSet<QString> files;
585✔
3377
  QDirIterator it( path, QStringList() << QStringLiteral( "*" ), QDir::Files, QDirIterator::Subdirectories );
1,755✔
3378
  while ( it.hasNext() )
2,244✔
3379
  {
3380
    it.next();
1,659✔
3381
    if ( !isInIgnore( it.fileInfo() ) )
1,659✔
3382
    {
3383
      files << it.filePath().replace( path, "" );
1,659✔
3384
    }
3385
  }
3386
  return files;
1,170✔
3387
}
585✔
3388

3389
void MerginApi::deleteAccount()
1✔
3390
{
3391
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
1✔
3392
  {
3393
    return;
×
3394
  }
3395

3396
  QNetworkRequest request = getDefaultRequest();
1✔
3397
  QUrl url( mApiRoot + QStringLiteral( "/v1/user" ) );
2✔
3398
  request.setUrl( url );
1✔
3399
  QNetworkReply *reply = mManager.deleteResource( request );
1✔
3400
  connect( reply, &QNetworkReply::finished, this, [this]() { this->deleteAccountFinished();} );
2✔
3401
  CoreUtils::log( "delete account " + mUserAuth->username(), QStringLiteral( "Requesting account deletion: " ) + url.toString() );
2✔
3402
}
1✔
3403

3404
void MerginApi::deleteAccountFinished()
1✔
3405
{
3406
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
1✔
3407
  Q_ASSERT( r );
1✔
3408

3409
  if ( r->error() == QNetworkReply::NoError )
1✔
3410
  {
3411
    CoreUtils::log( "delete account " + mUserAuth->username(), QStringLiteral( "Success" ) );
1✔
3412

3413
    // remove all local projects from the device
3414
    LocalProjectsList projects = mLocalProjects.projects();
1✔
3415
    for ( const LocalProject &info : projects )
36✔
3416
    {
3417
      mLocalProjects.removeLocalProject( info.id() );
35✔
3418
    }
3419

3420
    clearAuth();
1✔
3421

3422
    emit accountDeleted( true );
1✔
3423
  }
1✔
3424
  else
3425
  {
3426
    int statusCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
×
3427
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
3428
    CoreUtils::log( "delete account " + mUserAuth->username(), QStringLiteral( "FAILED - %1 %2. %3" ).arg( statusCode ).arg( r->errorString() ).arg( serverMsg ) );
×
3429
    if ( statusCode == 422 )
×
3430
    {
3431
      emit userIsAnOrgOwnerError();
×
3432
    }
3433
    else
3434
    {
3435
      emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: deleteAccount" ) );
×
3436
    }
3437

3438
    emit accountDeleted( false );
×
3439
  }
×
3440

3441
  r->deleteLater();
1✔
3442
}
1✔
3443

3444
void MerginApi::getServerConfig()
25✔
3445
{
3446
  QNetworkRequest request = getDefaultRequest();
25✔
3447
  QString urlString = mApiRoot + QStringLiteral( "/config" );
50✔
3448
  QUrl url( urlString );
25✔
3449
  request.setUrl( url );
25✔
3450

3451
  QNetworkReply *reply = mManager.get( request );
25✔
3452

3453
  connect( reply, &QNetworkReply::finished, this, &MerginApi::getServerConfigReplyFinished );
25✔
3454
  CoreUtils::log( "Config", QStringLiteral( "Requesting server configuration: " ) + url.toString() );
50✔
3455
}
25✔
3456

3457
void MerginApi::getServerConfigReplyFinished()
12✔
3458
{
3459
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
12✔
3460
  Q_ASSERT( r );
12✔
3461

3462
  if ( r->error() == QNetworkReply::NoError )
12✔
3463
  {
3464
    CoreUtils::log( "Config", QStringLiteral( "Success" ) );
12✔
3465
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
12✔
3466
    if ( doc.isObject() )
12✔
3467
    {
3468
      QString serverType = doc.object().value( QStringLiteral( "server_type" ) ).toString();
36✔
3469
      if ( serverType == QStringLiteral( "ee" ) )
12✔
3470
      {
3471
        setServerType( MerginServerType::EE );
×
3472
      }
3473
      else if ( serverType == QStringLiteral( "ce" ) )
12✔
3474
      {
3475
        setServerType( MerginServerType::CE );
×
3476
      }
3477
      else if ( serverType == QStringLiteral( "saas" ) )
12✔
3478
      {
3479
        setServerType( MerginServerType::SAAS );
12✔
3480
      }
3481
    }
12✔
3482
  }
12✔
3483
  else
3484
  {
3485
    int statusCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
×
3486
    if ( statusCode == 404 ) // legacy (old) server
×
3487
    {
3488
      setServerType( MerginServerType::OLD );
×
3489
    }
3490
    else
3491
    {
3492
      QString serverMsg = extractServerErrorMsg( r->readAll() );
×
3493
      QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "getServerType" ), r->errorString(), serverMsg );
×
3494
      emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: getServerType" ) );
×
3495
      CoreUtils::log( "server type", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
3496
    }
×
3497
  }
3498

3499
  r->deleteLater();
12✔
3500
}
12✔
3501

3502
MerginServerType::ServerType MerginApi::serverType() const
13✔
3503
{
3504
  return mServerType;
13✔
3505
}
3506

3507
void MerginApi::setServerType( const MerginServerType::ServerType &serverType )
17✔
3508
{
3509
  if ( mServerType != serverType )
17✔
3510
  {
3511
    if ( mServerType == MerginServerType::OLD && serverType == MerginServerType::SAAS )
6✔
3512
    {
3513
      emit serverWasUpgraded();
2✔
3514
    }
3515

3516
    mServerType = serverType;
6✔
3517
    QSettings settings;
6✔
3518
    settings.beginGroup( QStringLiteral( "Input/" ) );
6✔
3519
    settings.setValue( QStringLiteral( "serverType" ), mServerType );
12✔
3520
    settings.endGroup();
6✔
3521
    emit serverTypeChanged();
6✔
3522
    emit apiSupportsWorkspacesChanged();
6✔
3523
  }
6✔
3524
}
17✔
3525

3526
void MerginApi::listWorkspaces()
×
3527
{
3528
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
×
3529
  {
3530
    emit listWorkspacesFailed();
×
3531
    return;
×
3532
  }
3533

3534
  QUrl url( mApiRoot + QStringLiteral( "/v1/workspaces" ) );
×
3535
  QNetworkRequest request = getDefaultRequest( mUserAuth->hasAuthData() );
×
3536
  request.setUrl( url );
×
3537

3538
  QNetworkReply *reply = mManager.get( request );
×
3539
  CoreUtils::log( "list workspaces", QStringLiteral( "Requesting: " ) + url.toString() );
×
3540
  connect( reply, &QNetworkReply::finished, this, &MerginApi::listWorkspacesReplyFinished );
×
3541
}
×
3542

3543
void MerginApi::listWorkspacesReplyFinished()
×
3544
{
3545
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
×
3546
  Q_ASSERT( r );
×
3547

3548
  if ( r->error() == QNetworkReply::NoError )
×
3549
  {
3550
    CoreUtils::log( "list workspaces", QStringLiteral( "Success" ) );
×
3551
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
×
3552
    if ( doc.isArray() )
×
3553
    {
3554
      QMap<int, QString> workspaces;
×
3555
      QJsonArray array = doc.array();
×
3556
      for ( auto it = array.constBegin(); it != array.constEnd(); ++it )
×
3557
      {
3558
        QJsonObject ws = it->toObject();
×
3559
        workspaces.insert( ws.value( QStringLiteral( "id" ) ).toInt(), ws.value( QStringLiteral( "name" ) ).toString() );
×
3560
      }
×
3561

3562
      mUserInfo->setWorkspaces( workspaces );
×
3563
      emit listWorkspacesFinished( workspaces );
×
3564
    }
×
3565
    else
3566
    {
3567
      emit listWorkspacesFailed();
×
3568
    }
3569
  }
×
3570
  else
3571
  {
3572
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
3573
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listWorkspaces" ), r->errorString(), serverMsg );
×
3574
    CoreUtils::log( "list workspaces", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
3575
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: listWorkspaces" ) );
×
3576
    emit listWorkspacesFailed();
×
3577
  }
×
3578

3579
  r->deleteLater();
×
3580
}
×
3581

3582
void MerginApi::listInvitations()
×
3583
{
3584
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
×
3585
  {
3586
    emit listInvitationsFailed();
×
3587
    return;
×
3588
  }
3589

3590
  QUrl url( mApiRoot + QStringLiteral( "/v1/workspace/invitations" ) );
×
3591
  QNetworkRequest request = getDefaultRequest( mUserAuth->hasAuthData() );
×
3592
  request.setUrl( url );
×
3593

3594
  QNetworkReply *reply = mManager.get( request );
×
3595
  CoreUtils::log( "list invitations", QStringLiteral( "Requesting: " ) + url.toString() );
×
3596
  connect( reply, &QNetworkReply::finished, this, &MerginApi::listInvitationsReplyFinished );
×
3597
}
×
3598

3599
void MerginApi::listInvitationsReplyFinished()
×
3600
{
3601
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
×
3602
  Q_ASSERT( r );
×
3603

3604
  if ( r->error() == QNetworkReply::NoError )
×
3605
  {
3606
    CoreUtils::log( "list invitations", QStringLiteral( "Success" ) );
×
3607
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
×
3608
    if ( doc.isArray() )
×
3609
    {
3610
      QList<MerginInvitation> invitations;
×
3611
      QJsonArray array = doc.array();
×
3612
      for ( auto it = array.constBegin(); it != array.constEnd(); ++it )
×
3613
      {
3614
        MerginInvitation invite = MerginInvitation::fromJsonObject( it->toObject() );
×
3615
        invitations.append( invite );
×
3616
      }
×
3617

3618
      emit listInvitationsFinished( invitations );
×
3619
    }
×
3620
    else
3621
    {
3622
      emit listInvitationsFailed();
×
3623
    }
3624
  }
×
3625
  else
3626
  {
3627
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
3628
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listInvitations" ), r->errorString(), serverMsg );
×
3629
    CoreUtils::log( "list invitations", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
3630
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: listInvitations" ) );
×
3631
    emit listInvitationsFailed();
×
3632
  }
×
3633

3634
  r->deleteLater();
×
3635
}
×
3636

3637
void MerginApi::processInvitation( const QString &uuid, bool accept )
×
3638
{
3639
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
×
3640
  {
3641
    emit processInvitationFailed();
×
3642
    return;
×
3643
  }
3644

3645
  QNetworkRequest request = getDefaultRequest( true );
×
3646
  QString urlString = mApiRoot + QStringLiteral( "v1/workspace/invitation/%1" ).arg( uuid );
×
3647
  QUrl url( urlString );
×
3648
  request.setUrl( url );
×
3649
  request.setRawHeader( "Content-Type", "application/json" );
×
3650
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrAcceptFlag ), accept );
×
3651

3652
  QJsonDocument jsonDoc;
×
3653
  QJsonObject jsonObject;
×
3654
  jsonObject.insert( QStringLiteral( "accept" ), accept );
×
3655
  jsonDoc.setObject( jsonObject );
×
3656
  QByteArray json = jsonDoc.toJson( QJsonDocument::Compact );
×
3657
  QNetworkReply *reply = mManager.post( request, json );
×
3658
  CoreUtils::log( "process invitation", QStringLiteral( "Requesting: " ) + url.toString() );
×
3659
  connect( reply, &QNetworkReply::finished, this, &MerginApi::processInvitationReplyFinished );
×
3660
}
×
3661

3662
void MerginApi::processInvitationReplyFinished()
×
3663
{
3664
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
×
3665
  Q_ASSERT( r );
×
3666

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

3669
  if ( r->error() == QNetworkReply::NoError )
×
3670
  {
3671
    CoreUtils::log( "process invitation", QStringLiteral( "Success" ) );
×
3672
  }
3673
  else
3674
  {
3675
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
3676
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "processInvitation" ), r->errorString(), serverMsg );
×
3677
    CoreUtils::log( "process invitation", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
3678
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: processInvitation" ) );
×
3679
    emit processInvitationFailed();
×
3680
  }
×
3681

3682
  emit processInvitationFinished( accept );
×
3683

3684
  r->deleteLater();
×
3685
}
×
3686

3687
bool MerginApi::createWorkspace( const QString &workspaceName )
2✔
3688
{
3689
  if ( !validateAuth() )
2✔
3690
  {
3691
    emit missingAuthorizationError( workspaceName );
×
3692
    return false;
×
3693
  }
3694

3695
  if ( mApiVersionStatus != MerginApiStatus::OK )
2✔
3696
  {
3697
    return false;
×
3698
  }
3699

3700
  if ( !CoreUtils::isValidName( workspaceName ) )
2✔
3701
  {
3702
    emit notify( tr( "Workspace name contains invalid characters" ) );
×
3703
    return false;
×
3704
  }
3705

3706
  QNetworkRequest request = getDefaultRequest();
2✔
3707
  QUrl url( mApiRoot + QString( "/v1/workspace" ) );
4✔
3708
  request.setUrl( url );
2✔
3709
  request.setRawHeader( "Content-Type", "application/json" );
2✔
3710
  request.setRawHeader( "Accept", "application/json" );
2✔
3711
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrWorkspaceName ), workspaceName );
2✔
3712

3713
  QJsonDocument jsonDoc;
2✔
3714
  QJsonObject jsonObject;
2✔
3715
  jsonObject.insert( QStringLiteral( "name" ), workspaceName );
4✔
3716
  jsonDoc.setObject( jsonObject );
2✔
3717
  QByteArray json = jsonDoc.toJson( QJsonDocument::Compact );
2✔
3718

3719
  QNetworkReply *reply = mManager.post( request, json );
2✔
3720
  connect( reply, &QNetworkReply::finished, this, &MerginApi::createWorkspaceReplyFinished );
2✔
3721
  CoreUtils::log( "create " + workspaceName, QStringLiteral( "Requesting workspace creation: " ) + url.toString() );
4✔
3722

3723
  return true;
2✔
3724
}
2✔
3725

3726
void MerginApi::signOut()
×
3727
{
3728
  clearAuth();
×
3729
}
×
3730

3731
void MerginApi::refreshUserData()
×
3732
{
3733
  getUserInfo();
×
3734

3735
  if ( apiSupportsWorkspaces() )
×
3736
  {
3737
    getWorkspaceInfo();
×
3738
    // getServiceInfo is called automatically when workspace info finishes
3739
  }
3740
  else if ( mServerType == MerginServerType::OLD )
×
3741
  {
3742
    getServiceInfo();
×
3743
  }
3744
}
×
3745

3746
void MerginApi::createWorkspaceReplyFinished()
2✔
3747
{
3748
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
2✔
3749
  Q_ASSERT( r );
2✔
3750

3751
  QString workspaceName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrWorkspaceName ) ).toString();
4✔
3752

3753
  if ( r->error() == QNetworkReply::NoError )
2✔
3754
  {
3755
    CoreUtils::log( "create " + workspaceName, QStringLiteral( "Success" ) );
2✔
3756
    emit workspaceCreated( workspaceName, true );
2✔
3757
  }
3758
  else
3759
  {
3760
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
3761
    QString message = QStringLiteral( "FAILED - %1: %2" ).arg( r->errorString(), serverMsg );
×
3762
    CoreUtils::log( "create " + workspaceName, message );
×
3763

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

3766
    if ( httpCode == 409 )
×
3767
    {
3768
      emit networkErrorOccurred( tr( "Workspace %1 already exists" ).arg( workspaceName ), QStringLiteral( "Mergin API error: createWorkspace" ), httpCode, workspaceName );
×
3769
    }
3770
    else
3771
    {
3772
      emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: createWorkspace" ), httpCode, workspaceName );
×
3773
    }
3774
    emit workspaceCreated( workspaceName, false );
×
3775
  }
×
3776
  r->deleteLater();
2✔
3777
}
2✔
3778

3779
bool MerginApi::apiSupportsWorkspaces()
1✔
3780
{
3781
  if ( mServerType == MerginServerType::SAAS || mServerType == MerginServerType::EE )
1✔
3782
  {
3783
    return true;
1✔
3784
  }
3785
  else
3786
  {
3787
    return false;
×
3788
  }
3789
}
3790

3791
DownloadQueueItem::DownloadQueueItem( const QString &fp, int s, int v, int rf, int rt, bool diff )
153✔
3792
  : filePath( fp ), size( s ), version( v ), rangeFrom( rf ), rangeTo( rt ), downloadDiff( diff )
153✔
3793
{
3794
  tempFileName = CoreUtils::uuidWithoutBraces( QUuid::createUuid() );
153✔
3795
}
153✔
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