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

MerginMaps / input / 6690232974

30 Oct 2023 08:34AM UTC coverage: 62.205% (-0.04%) from 62.243%
6690232974

Pull #2882

github

wonder-sk
code style
Pull Request #2882: Download data in parallel during sync

7686 of 12356 relevant lines covered (62.2%)

105.07 hits per line

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

79.08
/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,079✔
305
{
306
  QNetworkRequest request;
1,079✔
307
  QString info = CoreUtils::appInfo();
1,079✔
308
  request.setRawHeader( "User-Agent", QByteArray( info.toUtf8() ) );
1,079✔
309
  if ( withAuth )
1,079✔
310
    request.setRawHeader( "Authorization", QByteArray( "Bearer " + mUserAuth->authToken() ) );
1,039✔
311

312
  return request;
2,158✔
313
}
1,079✔
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;
×
884
  }
885

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

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

900
  QNetworkReply *reply = mManager.get( request );
20✔
901
  CoreUtils::log( "user info", QStringLiteral( "Requesting user info: " ) + url.toString() );
40✔
902
  connect( reply, &QNetworkReply::finished, this, &MerginApi::getUserInfoFinished );
20✔
903
}
20✔
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;
×
1075
  }
1076

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

1079
  QNetworkRequest request = getDefaultRequest();
43✔
1080
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/%1" ).arg( projectFullName ) );
86✔
1081
  request.setUrl( url );
43✔
1082
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
43✔
1083
  QNetworkReply *reply = mManager.deleteResource( request );
43✔
1084
  connect( reply, &QNetworkReply::finished, this, [this, informUser]() { this->deleteProjectFinished( informUser );} );
86✔
1085
  CoreUtils::log( "delete " + projectFullName, QStringLiteral( "Requesting project deletion: " ) + url.toString() );
86✔
1086
}
43✔
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 )
43✔
1161
{
1162
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
43✔
1163
  Q_ASSERT( r );
43✔
1164

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

1167
  if ( r->error() == QNetworkReply::NoError )
43✔
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() );
82✔
1179
    CoreUtils::log( "delete " + projectFullName, QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) );
41✔
1180
    emit serverProjectDeleted( projectFullName, false );
41✔
1181
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: deleteProject" ) );
82✔
1182
  }
41✔
1183
  r->deleteLater();
43✔
1184
}
43✔
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();
45✔
1471
    if ( obj.contains( QStringLiteral( "detail" ) ) )
45✔
1472
    {
1473
      QJsonValue vDetail = obj.value( "detail" );
43✔
1474
      if ( vDetail.isString() )
43✔
1475
      {
1476
        serverMsg = vDetail.toString();
43✔
1477
      }
1478
      else if ( vDetail.isObject() )
×
1479
      {
1480
        serverMsg = QJsonDocument( vDetail.toObject() ).toJson();
×
1481
      }
1482
    }
43✔
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
  }
45✔
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 );
5✔
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,967✔
1543
{
1544
  return QString( "%1/%2" ).arg( projectNamespace ).arg( projectName );
43,967✔
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
  // order download queue from the largest to smallest chunks to better
2328
  // work with parallel downloads
2329
  std::sort(
124✔
2330
    transaction.downloadQueue.begin(), transaction.downloadQueue.end(),
2331
  []( const DownloadQueueItem & a, const DownloadQueueItem & b ) { return a.size > b.size; }
120✔
2332
  );
2333

2334
  CoreUtils::log( "pull " + projectFullName, QStringLiteral( "%1 update tasks, %2 items to download (total size %3 bytes)" )
372✔
2335
                  .arg( transaction.pullTasks.count() )
248✔
2336
                  .arg( transaction.downloadQueue.count() )
248✔
2337
                  .arg( transaction.totalSize ) );
248✔
2338

2339
  emit pullFilesStarted();
124✔
2340

2341
  if ( transaction.downloadQueue.isEmpty() )
124✔
2342
  {
2343
    finalizeProjectPull( projectFullName );
34✔
2344
  }
2345
  else
2346
  {
2347
    while ( transaction.replyPullItems.count() < 5 && !transaction.downloadQueue.isEmpty() )
243✔
2348
    {
2349
      downloadNextItem( projectFullName );
153✔
2350
    }
2351
  }
2352
}
124✔
2353

2354
void MerginApi::prepareDownloadConfig( const QString &projectFullName, bool downloaded )
151✔
2355
{
2356
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
151✔
2357
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
151✔
2358

2359
  MerginProjectMetadata newServerVersion = MerginProjectMetadata::fromJson( transaction.projectMetadata );
151✔
2360

2361
  const auto res = std::find_if( newServerVersion.files.begin(), newServerVersion.files.end(), []( const MerginFile & file )
151✔
2362
  {
2363
    return file.path == sMerginConfigFile;
531✔
2364
  } );
2365
  bool serverContainsConfig = res != newServerVersion.files.end();
151✔
2366

2367
  if ( serverContainsConfig )
151✔
2368
  {
2369
    if ( !downloaded )
78✔
2370
    {
2371
      // we should have server config but we do not have it yet
2372
      return requestServerConfig( projectFullName );
39✔
2373
    }
2374
  }
2375

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

2378
  const auto resOld = std::find_if( oldServerVersion.files.begin(), oldServerVersion.files.end(), []( const MerginFile & file )
112✔
2379
  {
2380
    return file.path == sMerginConfigFile;
205✔
2381
  } );
2382

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

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

2397
    if ( newChk == oldChk )
29✔
2398
    {
2399
      // config files are the same
2400
    }
2401
    else
2402
    {
2403
      // config was changed, but what changed?
2404
      MerginConfig oldConfig = MerginConfig::fromFile( transaction.projectDir + "/" + MerginApi::sMerginConfigFile );
16✔
2405

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

2445
    // if it would be possible to add mergin-config locally, it needs to be checked here
2446
  }
2447

2448
  startProjectPull( projectFullName );
112✔
2449
}
151✔
2450

2451
void MerginApi::requestServerConfig( const QString &projectFullName )
39✔
2452
{
2453
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
39✔
2454
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
39✔
2455

2456
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/raw/" ) + projectFullName );
78✔
2457
  QUrlQuery query;
39✔
2458

2459
  query.addQueryItem( "file", sMerginConfigFile.toUtf8().toPercentEncoding() );
39✔
2460
  query.addQueryItem( "version", QStringLiteral( "v%1" ).arg( transaction.version ) );
39✔
2461
  url.setQuery( query );
39✔
2462

2463
  QNetworkRequest request = getDefaultRequest();
39✔
2464
  request.setUrl( url );
39✔
2465
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
39✔
2466

2467
  Q_ASSERT( !transaction.replyPullServerConfig );
39✔
2468
  transaction.replyPullServerConfig = mManager.get( request );
39✔
2469
  connect( transaction.replyPullServerConfig, &QNetworkReply::finished, this, &MerginApi::cacheServerConfig );
39✔
2470

2471
  CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Requesting mergin config: " ) + url.toString() );
78✔
2472
}
39✔
2473

2474
QList<DownloadQueueItem> MerginApi::itemsForFileChunks( const MerginFile &file, int version )
204✔
2475
{
2476
  QList<DownloadQueueItem> lst;
204✔
2477
  int from = 0;
204✔
2478
  while ( from < file.size )
347✔
2479
  {
2480
    int size = qMin( MerginApi::UPLOAD_CHUNK_SIZE, static_cast<int>( file.size ) - from );
143✔
2481
    lst << DownloadQueueItem( file.path, size, version, from, from + size - 1 );
143✔
2482
    from += size;
143✔
2483
  }
2484
  return lst;
204✔
2485
}
×
2486

2487
QList<DownloadQueueItem> MerginApi::itemsForFileDiffs( const MerginFile &file )
9✔
2488
{
2489
  QList<DownloadQueueItem> items;
9✔
2490
  // download diffs instead of full download of gpkg file from server
2491
  for ( const auto &d : file.pullDiffFiles )
19✔
2492
  {
2493
    items << DownloadQueueItem( file.path, d.second, d.first, -1, -1, true );
10✔
2494
  }
2495
  return items;
9✔
2496
}
×
2497

2498

2499
static MerginFile findFile( const QString &filePath, const QList<MerginFile> &files )
155✔
2500
{
2501
  for ( const MerginFile &merginFile : files )
430✔
2502
  {
2503
    if ( merginFile.path == filePath )
430✔
2504
      return merginFile;
155✔
2505
  }
2506
  CoreUtils::log( QStringLiteral( "MerginFile" ), QStringLiteral( "requested findFile() for non-existant file: %1" ).arg( filePath ) );
×
2507
  return MerginFile();
×
2508
}
2509

2510

2511
void MerginApi::pushInfoReplyFinished()
143✔
2512
{
2513
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
143✔
2514
  Q_ASSERT( r );
143✔
2515

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

2518
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
143✔
2519
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
143✔
2520
  Q_ASSERT( r == transaction.replyPushProjectInfo );
143✔
2521

2522
  if ( r->error() == QNetworkReply::NoError )
143✔
2523
  {
2524
    QString url = r->url().toString();
142✔
2525
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Downloaded project info." ) );
142✔
2526
    QByteArray data = r->readAll();
142✔
2527

2528
    transaction.replyPushProjectInfo->deleteLater();
142✔
2529
    transaction.replyPushProjectInfo = nullptr;
142✔
2530

2531
    LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName );
142✔
2532
    transaction.projectDir = projectInfo.projectDir;
142✔
2533
    Q_ASSERT( !transaction.projectDir.isEmpty() );
142✔
2534

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

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

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

2552
    // Cache mergin-config, since we are on the most recent version, it is sufficient to just read the local version
2553
    if ( transaction.configAllowed )
116✔
2554
    {
2555
      transaction.config = MerginConfig::fromFile( transaction.projectDir + "/" + MerginApi::sMerginConfigFile );
107✔
2556
    }
2557

2558
    transaction.diff = compareProjectFiles(
232✔
2559
                         oldServerProject.files,
2560
                         serverProject.files,
2561
                         localFiles,
2562
                         transaction.projectDir,
116✔
2563
                         transaction.configAllowed,
116✔
2564
                         transaction.config
116✔
2565
                       );
116✔
2566

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

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

2571
    QList<MerginFile> filesToUpload;
116✔
2572
    QList<MerginFile> addedMerginFiles, updatedMerginFiles, deletedMerginFiles;
116✔
2573
    QList<MerginFile> diffFiles;
116✔
2574
    for ( QString filePath : transaction.diff.localAdded )
248✔
2575
    {
2576
      MerginFile merginFile = findFile( filePath, localFiles );
132✔
2577
      merginFile.chunks = generateChunkIdsForSize( merginFile.size );
132✔
2578
      addedMerginFiles.append( merginFile );
132✔
2579
    }
132✔
2580

2581
    for ( QString filePath : transaction.diff.localUpdated )
135✔
2582
    {
2583
      MerginFile merginFile = findFile( filePath, localFiles );
19✔
2584
      merginFile.chunks = generateChunkIdsForSize( merginFile.size );
19✔
2585

2586
      if ( MerginApi::isFileDiffable( filePath ) )
19✔
2587
      {
2588
        // try to create a diff
2589
        QString diffName;
12✔
2590
        int geodiffRes = GeodiffUtils::createChangeset( transaction.projectDir, filePath, diffName );
12✔
2591
        QString diffPath = transaction.projectDir + "/.mergin/" + diffName;
12✔
2592
        QString basePath = transaction.projectDir + "/.mergin/" + filePath;
12✔
2593

2594
        if ( geodiffRes == GEODIFF_SUCCESS )
12✔
2595
        {
2596
          QByteArray checksumDiff = CoreUtils::calculateChecksum( diffPath );
12✔
2597

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

2602
          merginFile.diffName = diffName;
12✔
2603
          merginFile.diffChecksum = QString::fromLatin1( checksumDiff.data(), checksumDiff.size() );
12✔
2604
          merginFile.diffSize = QFileInfo( diffPath ).size();
12✔
2605
          merginFile.chunks = generateChunkIdsForSize( merginFile.diffSize );
12✔
2606
          merginFile.diffBaseChecksum = QString::fromLatin1( checksumBase.data(), checksumBase.size() );
12✔
2607

2608
          diffFiles.append( merginFile );
12✔
2609

2610
          CoreUtils::log( "push " + projectFullName, QString( "Geodiff create changeset on %1 successful: total size %2 bytes" ).arg( filePath ).arg( merginFile.diffSize ) );
12✔
2611
        }
12✔
2612
        else
2613
        {
2614
          // TODO: remove the diff file (if exists)
2615
          CoreUtils::log( "push " + projectFullName, QString( "Geodiff create changeset on %1 FAILED with error %2 (will do full upload)" ).arg( filePath ).arg( geodiffRes ) );
×
2616
        }
2617
      }
12✔
2618

2619
      updatedMerginFiles.append( merginFile );
19✔
2620
    }
19✔
2621

2622
    for ( QString filePath : transaction.diff.localDeleted )
120✔
2623
    {
2624
      MerginFile merginFile = findFile( filePath, serverProject.files );
4✔
2625
      deletedMerginFiles.append( merginFile );
4✔
2626
    }
4✔
2627

2628
    if ( addedMerginFiles.isEmpty() && updatedMerginFiles.isEmpty() && deletedMerginFiles.isEmpty() )
116✔
2629
    {
2630
      // if nothing has changed, there is no point to even start upload transaction
2631
      transaction.projectMetadata = data;
1✔
2632
      transaction.version = MerginProjectMetadata::fromJson( data ).version;
1✔
2633

2634
      finishProjectSync( projectFullName, true );
1✔
2635
      return;
1✔
2636
    }
2637

2638
    QJsonArray added = prepareUploadChangesJSON( addedMerginFiles );
115✔
2639
    filesToUpload.append( addedMerginFiles );
115✔
2640

2641
    QJsonArray modified = prepareUploadChangesJSON( updatedMerginFiles );
115✔
2642
    filesToUpload.append( updatedMerginFiles );
115✔
2643

2644
    QJsonArray removed = prepareUploadChangesJSON( deletedMerginFiles );
115✔
2645
    // removed not in filesToUpload
2646

2647
    QJsonObject changes;
115✔
2648
    changes.insert( "added", added );
115✔
2649
    changes.insert( "removed", removed );
115✔
2650
    changes.insert( "updated", modified );
115✔
2651
    changes.insert( "renamed", QJsonArray() );
115✔
2652

2653
    qint64 totalSize = 0;
115✔
2654
    for ( MerginFile file : filesToUpload )
266✔
2655
    {
2656
      if ( !file.diffName.isEmpty() )
151✔
2657
        totalSize += file.diffSize;
12✔
2658
      else
2659
        totalSize += file.size;
139✔
2660
    }
151✔
2661

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

2665
    transaction.totalSize = totalSize;
115✔
2666
    transaction.pushQueue = filesToUpload;
115✔
2667
    transaction.pushDiffFiles = diffFiles;
115✔
2668

2669
    QJsonObject json;
115✔
2670
    json.insert( QStringLiteral( "changes" ), changes );
230✔
2671
    json.insert( QStringLiteral( "version" ), QString( "v%1" ).arg( serverProject.version ) );
230✔
2672
    QJsonDocument jsonDoc;
115✔
2673
    jsonDoc.setObject( json );
115✔
2674

2675
    pushStart( projectFullName, jsonDoc.toJson( QJsonDocument::Compact ) );
115✔
2676
  }
230✔
2677
  else
2678
  {
2679
    QString serverMsg = extractServerErrorMsg( r->readAll() );
2✔
2680
    QString message = QStringLiteral( "Network API error: %1(): %2" ).arg( QStringLiteral( "projectInfo" ), r->errorString() );
3✔
2681
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) );
1✔
2682

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

2686
    transaction.replyPushProjectInfo->deleteLater();
1✔
2687
    transaction.replyPushProjectInfo = nullptr;
1✔
2688

2689
    finishProjectSync( projectFullName, false );
1✔
2690
  }
1✔
2691
}
143✔
2692

2693
void MerginApi::pushFinishReplyFinished()
110✔
2694
{
2695
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
110✔
2696
  Q_ASSERT( r );
110✔
2697

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

2700
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
110✔
2701
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
110✔
2702
  Q_ASSERT( r == transaction.replyPushFinish );
110✔
2703

2704
  if ( r->error() == QNetworkReply::NoError )
110✔
2705
  {
2706
    Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
110✔
2707
    QByteArray data = r->readAll();
110✔
2708
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Transaction finish accepted" ) );
110✔
2709

2710
    transaction.replyPushFinish->deleteLater();
110✔
2711
    transaction.replyPushFinish = nullptr;
110✔
2712

2713
    transaction.projectMetadata = data;
110✔
2714
    transaction.version = MerginProjectMetadata::fromJson( data ).version;
110✔
2715

2716
    //  a new diffable files suppose to have their basefile copies in .mergin
2717
    for ( QString filePath : transaction.diff.localAdded )
240✔
2718
    {
2719
      if ( MerginApi::isFileDiffable( filePath ) )
130✔
2720
      {
2721
        QString basefile = transaction.projectDir + "/.mergin/" + filePath;
11✔
2722
        createPathIfNotExists( basefile );
11✔
2723

2724
        QString sourcePath = transaction.projectDir + "/" + filePath;
11✔
2725
        if ( !QFile::copy( sourcePath, basefile ) )
11✔
2726
        {
2727
          CoreUtils::log( "push " + projectFullName, "failed to copy new basefile for: " + filePath );
×
2728
        }
2729
      }
11✔
2730
    }
130✔
2731

2732
    // clean up diff-related files
2733
    const auto diffFiles = transaction.pushDiffFiles;
110✔
2734
    for ( const MerginFile &merginFile : diffFiles )
122✔
2735
    {
2736
      QString diffPath = transaction.projectDir + "/.mergin/" + merginFile.diffName;
12✔
2737

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

2750
      // remove temporary diff files
2751
      if ( !QFile::remove( diffPath ) )
12✔
2752
        CoreUtils::log( "push " + projectFullName, "Failed to remove diff: " + diffPath );
×
2753
    }
12✔
2754

2755
    finishProjectSync( projectFullName, true );
110✔
2756
  }
110✔
2757
  else
2758
  {
2759
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
2760
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "pushFinish" ), r->errorString(), serverMsg );
×
2761
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) );
×
2762

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

2766
    // remove temporary diff files
2767
    const auto diffFiles = transaction.pushDiffFiles;
×
2768
    for ( const MerginFile &merginFile : diffFiles )
×
2769
    {
2770
      QString diffPath = transaction.projectDir + "/.mergin/" + merginFile.diffName;
×
2771
      if ( !QFile::remove( diffPath ) )
×
2772
        CoreUtils::log( "push " + projectFullName, "Failed to remove diff: " + diffPath );
×
2773
    }
×
2774

2775
    transaction.replyPushFinish->deleteLater();
×
2776
    transaction.replyPushFinish = nullptr;
×
2777

2778
    finishProjectSync( projectFullName, false );
×
2779
  }
×
2780
}
110✔
2781

2782
void MerginApi::pushCancelReplyFinished()
1✔
2783
{
2784
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
1✔
2785
  Q_ASSERT( r );
1✔
2786

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

2789
  if ( r->error() == QNetworkReply::NoError )
1✔
2790
  {
2791
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Transaction canceled" ) );
1✔
2792
  }
2793
  else
2794
  {
2795
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
2796
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "uploadCancel" ), r->errorString(), serverMsg );
×
2797
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) );
×
2798
  }
×
2799

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

2802
  r->deleteLater();
1✔
2803
}
1✔
2804

2805
void MerginApi::getUserInfoFinished()
20✔
2806
{
2807
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
20✔
2808
  Q_ASSERT( r );
20✔
2809

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

2833
  emit userInfoReplyFinished();
20✔
2834

2835
  r->deleteLater();
20✔
2836
}
20✔
2837

2838
void MerginApi::getWorkspaceInfoReplyFinished()
25✔
2839
{
2840
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
25✔
2841
  Q_ASSERT( r );
25✔
2842

2843
  if ( r->error() == QNetworkReply::NoError )
25✔
2844
  {
2845
    CoreUtils::log( "workspace info", QStringLiteral( "Success" ) );
25✔
2846
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
25✔
2847
    if ( doc.isObject() )
25✔
2848
    {
2849
      QJsonObject docObj = doc.object();
25✔
2850
      mWorkspaceInfo->setFromJson( docObj );
25✔
2851

2852
      emit getWorkspaceInfoFinished();
25✔
2853
    }
25✔
2854
  }
25✔
2855
  else
2856
  {
2857
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
2858
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "getWorkspaceInfo" ), r->errorString(), serverMsg );
×
2859
    CoreUtils::log( "workspace info", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
2860
    mWorkspaceInfo->clear();
×
2861
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: getWorkspaceInfo" ) );
×
2862
  }
×
2863

2864
  r->deleteLater();
25✔
2865
}
25✔
2866

2867
bool MerginApi::hasLocalChanges(
328✔
2868
  const QList<MerginFile> &oldServerFiles,
2869
  const QList<MerginFile> &localFiles,
2870
  const QString &projectDir
2871
)
2872
{
2873
  if ( localFiles.count() != oldServerFiles.count() )
328✔
2874
  {
2875
    return true;
24✔
2876
  }
2877

2878
  QHash<QString, MerginFile> oldServerFilesMap;
304✔
2879

2880
  for ( const MerginFile &file : oldServerFiles )
970✔
2881
  {
2882
    oldServerFilesMap.insert( file.path, file );
666✔
2883
  }
2884

2885
  for ( const MerginFile &localFile : localFiles )
956✔
2886
  {
2887
    QString filePath = localFile.path;
665✔
2888
    bool hasOldServer = oldServerFilesMap.contains( localFile.path );
665✔
2889

2890
    if ( !hasOldServer )
665✔
2891
    {
2892
      // L-A
2893
      return true;
×
2894
    }
2895
    else
2896
    {
2897
      const QString chkOld = oldServerFilesMap.value( localFile.path ).checksum;
665✔
2898
      const QString chkLocal = localFile.checksum;
665✔
2899

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

2922
  // We know that the number of local files and old server is the same
2923
  // And also that all local files has old file counterpart
2924
  // So it is not possible that there is deleted local file at this point.
2925
  return false;
291✔
2926
}
304✔
2927

2928
ProjectDiff MerginApi::compareProjectFiles(
257✔
2929
  const QList<MerginFile> &oldServerFiles,
2930
  const QList<MerginFile> &newServerFiles,
2931
  const QList<MerginFile> &localFiles,
2932
  const QString &projectDir,
2933
  bool allowConfig,
2934
  const MerginConfig &config,
2935
  const MerginConfig &lastSyncConfig
2936
)
2937
{
2938
  ProjectDiff diff;
257✔
2939
  QHash<QString, MerginFile> oldServerFilesMap, newServerFilesMap;
257✔
2940

2941
  for ( MerginFile file : newServerFiles )
1,384✔
2942
  {
2943
    newServerFilesMap.insert( file.path, file );
1,127✔
2944
  }
1,127✔
2945
  for ( MerginFile file : oldServerFiles )
1,178✔
2946
  {
2947
    oldServerFilesMap.insert( file.path, file );
921✔
2948
  }
921✔
2949

2950
  for ( MerginFile localFile : localFiles )
1,164✔
2951
  {
2952
    QString filePath = localFile.path;
907✔
2953
    bool hasOldServer = oldServerFilesMap.contains( localFile.path );
907✔
2954
    bool hasNewServer = newServerFilesMap.contains( localFile.path );
907✔
2955
    QString chkOld = oldServerFilesMap.value( localFile.path ).checksum;
907✔
2956
    QString chkNew = newServerFilesMap.value( localFile.path ).checksum;
907✔
2957
    QString chkLocal = localFile.checksum;
907✔
2958

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

3045
    if ( hasOldServer )
907✔
3046
      oldServerFilesMap.remove( filePath );
747✔
3047
    if ( hasNewServer )
907✔
3048
      newServerFilesMap.remove( filePath );
744✔
3049
  }
907✔
3050

3051
  // go through files listed on the server, but not available locally
3052
  for ( MerginFile file : newServerFilesMap )
640✔
3053
  {
3054
    bool hasOldServer = oldServerFilesMap.contains( file.path );
383✔
3055

3056
    if ( hasOldServer )
383✔
3057
    {
3058
      if ( oldServerFilesMap.value( file.path ).checksum == file.checksum )
174✔
3059
      {
3060
        // L-D
3061
        if ( allowConfig )
174✔
3062
        {
3063
          bool shouldBeExcludedFromSync = MerginApi::excludeFromSync( file.path, config );
173✔
3064
          if ( shouldBeExcludedFromSync )
173✔
3065
          {
3066
            continue;
151✔
3067
          }
3068

3069
          // check if we should download missing files that were previously ignored (e.g. selective sync has been disabled)
3070
          bool previouslyIgnoredButShouldDownload = \
3071
              config.downloadMissingFiles &&
41✔
3072
              lastSyncConfig.isValid &&
41✔
3073
              MerginApi::excludeFromSync( file.path, lastSyncConfig );
19✔
3074

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

3102
    if ( hasOldServer )
178✔
3103
      oldServerFilesMap.remove( file.path );
4✔
3104
  }
383✔
3105

3106
  /*
3107
  for ( MerginFile file : oldServerFilesMap )
3108
  {
3109
    // R-D/L-D
3110
    // TODO: need to do anything?
3111
  }
3112
  */
3113

3114
  return diff;
514✔
3115
}
257✔
3116

3117
MerginProject MerginApi::parseProjectMetadata( const QJsonObject &proj )
286✔
3118
{
3119
  MerginProject project;
286✔
3120

3121
  if ( proj.isEmpty() )
286✔
3122
  {
3123
    return project;
×
3124
  }
3125

3126
  if ( proj.contains( QStringLiteral( "error" ) ) )
286✔
3127
  {
3128
    // handle project error (user might be logged out / do not have write rights / project is on different server / project is orphaned)
3129
    project.remoteError = QString::number( proj.value( QStringLiteral( "error" ) ).toInt( 0 ) ); // error code
×
3130
    return project;
×
3131
  }
3132

3133
  project.projectName = proj.value( QStringLiteral( "name" ) ).toString();
286✔
3134
  project.projectNamespace = proj.value( QStringLiteral( "namespace" ) ).toString();
286✔
3135

3136
  QString versionStr = proj.value( QStringLiteral( "version" ) ).toString();
572✔
3137
  if ( versionStr.isEmpty() )
286✔
3138
  {
3139
    project.serverVersion = 0;
×
3140
  }
3141
  else if ( versionStr.startsWith( "v" ) ) // cut off 'v' part from v123
286✔
3142
  {
3143
    versionStr = versionStr.mid( 1 );
286✔
3144
    project.serverVersion = versionStr.toInt();
286✔
3145
  }
3146

3147
  QDateTime updated = QDateTime::fromString( proj.value( QStringLiteral( "updated" ) ).toString(), Qt::ISODateWithMs ).toUTC();
572✔
3148
  if ( !updated.isValid() )
286✔
3149
  {
3150
    project.serverUpdated = QDateTime::fromString( proj.value( QStringLiteral( "created" ) ).toString(), Qt::ISODateWithMs ).toUTC();
×
3151
  }
3152
  else
3153
  {
3154
    project.serverUpdated = updated;
286✔
3155
  }
3156
  return project;
286✔
3157
}
286✔
3158

3159

3160
MerginProjectsList MerginApi::parseProjectsFromJson( const QJsonDocument &doc )
29✔
3161
{
3162
  if ( !doc.isObject() )
29✔
3163
    return MerginProjectsList();
×
3164

3165
  QJsonObject object = doc.object();
29✔
3166
  MerginProjectsList result;
29✔
3167

3168
  if ( object.contains( "projects" ) && object.value( "projects" ).isArray() ) // listProjects API
29✔
3169
  {
3170
    QJsonArray vArray = object.value( "projects" ).toArray();
44✔
3171

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

3193
void MerginApi::refreshAuthToken()
7✔
3194
{
3195
  if ( !mUserAuth->hasAuthData() ||
14✔
3196
       mUserAuth->authToken().isEmpty() )
14✔
3197
  {
3198
    CoreUtils::log( QStringLiteral( "Auth" ), QStringLiteral( "Can not refresh token, missing credentials" ) );
×
3199
    return;
×
3200
  }
3201

3202
  if ( mUserAuth->tokenExpiration() < QDateTime::currentDateTimeUtc() )
7✔
3203
  {
3204
    CoreUtils::log( QStringLiteral( "Auth" ), QStringLiteral( "Token has expired, requesting new one" ) );
×
3205
    authorize( mUserAuth->username(), mUserAuth->password() );
×
3206
    mAuthLoopEvent.exec();
×
3207
  }
3208
}
3209

3210
QStringList MerginApi::generateChunkIdsForSize( qint64 fileSize )
163✔
3211
{
3212
  qreal rawNoOfChunks = qreal( fileSize ) / UPLOAD_CHUNK_SIZE;
163✔
3213
  int noOfChunks = qCeil( rawNoOfChunks );
163✔
3214

3215
  // edge case when file is empty, filesize equals zero
3216
  // manually set one chunk so that file will be synced
3217
  if ( fileSize <= 0 )
163✔
3218
    noOfChunks = 1;
45✔
3219

3220
  QStringList chunks;
163✔
3221
  for ( int i = 0; i < noOfChunks; i++ )
328✔
3222
  {
3223
    QString chunkID = CoreUtils::uuidWithoutBraces( QUuid::createUuid() );
165✔
3224
    chunks.append( chunkID );
165✔
3225
  }
165✔
3226
  return chunks;
163✔
3227
}
×
3228

3229
QJsonArray MerginApi::prepareUploadChangesJSON( const QList<MerginFile> &files )
345✔
3230
{
3231
  QJsonArray jsonArray;
345✔
3232

3233
  for ( MerginFile file : files )
500✔
3234
  {
3235
    QJsonObject fileObject;
155✔
3236
    fileObject.insert( "path", file.path );
155✔
3237

3238
    fileObject.insert( "size", file.size );
155✔
3239
    fileObject.insert( "mtime", file.mtime.toString( Qt::ISODateWithMs ) );
155✔
3240

3241
    if ( !file.diffName.isEmpty() )
155✔
3242
    {
3243
      // doing diff-based upload
3244
      QJsonObject diffObject;
12✔
3245
      diffObject.insert( "path", file.diffName );
12✔
3246
      diffObject.insert( "checksum", file.diffChecksum );
12✔
3247
      diffObject.insert( "size", file.diffSize );
12✔
3248

3249
      fileObject.insert( "diff", diffObject );
12✔
3250
      fileObject.insert( "checksum", file.diffBaseChecksum );
12✔
3251
    }
12✔
3252
    else
3253
    {
3254
      fileObject.insert( "checksum", file.checksum );
143✔
3255
    }
3256

3257
    QJsonArray chunksJson;
155✔
3258
    for ( QString id : file.chunks )
308✔
3259
    {
3260
      chunksJson.append( id );
153✔
3261
    }
153✔
3262
    fileObject.insert( "chunks", chunksJson );
155✔
3263
    jsonArray.append( fileObject );
155✔
3264
  }
155✔
3265
  return jsonArray;
345✔
3266
}
×
3267

3268
void MerginApi::finishProjectSync( const QString &projectFullName, bool syncSuccessful )
243✔
3269
{
3270
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
243✔
3271
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
243✔
3272

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

3275
  if ( syncSuccessful )
243✔
3276
  {
3277
    // update the local metadata file
3278
    writeData( transaction.projectMetadata, transaction.projectDir + "/" + MerginApi::sMetadataFile );
238✔
3279

3280
    // update info of local projects
3281
    mLocalProjects.updateLocalVersion( transaction.projectDir, transaction.version );
238✔
3282

3283
    CoreUtils::log( "sync " + projectFullName, QStringLiteral( "### Finished ###  New project version: %1\n" ).arg( transaction.version ) );
238✔
3284
  }
3285
  else
3286
  {
3287
    CoreUtils::log( "sync " + projectFullName, QStringLiteral( "### FAILED ###\n" ) );
5✔
3288
  }
3289

3290
  bool pullBeforePush = transaction.pullBeforePush;
243✔
3291
  QString projectDir = transaction.projectDir;  // keep it before the transaction gets removed
243✔
3292
  ProjectDiff diff = transaction.diff;
243✔
3293
  int newVersion = syncSuccessful ? transaction.version : -1;
243✔
3294

3295
  if ( transaction.gpkgSchemaChanged || projectFileHasBeenUpdated( diff ) )
243✔
3296
  {
3297
    emit projectReloadNeededAfterSync( projectFullName );
96✔
3298
  }
3299

3300
  mTransactionalStatus.remove( projectFullName );
243✔
3301

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

3314
    if ( syncSuccessful )
217✔
3315
    {
3316
      emit projectDataChanged( projectFullName );
212✔
3317
    }
3318
  }
3319
}
243✔
3320

3321
bool MerginApi::writeData( const QByteArray &data, const QString &path )
238✔
3322
{
3323
  QFile file( path );
238✔
3324
  createPathIfNotExists( path );
238✔
3325
  if ( !file.open( QIODevice::WriteOnly ) )
238✔
3326
  {
3327
    return false;
×
3328
  }
3329

3330
  file.write( data );
238✔
3331
  file.close();
238✔
3332

3333
  return true;
238✔
3334
}
238✔
3335

3336

3337
void MerginApi::createPathIfNotExists( const QString &filePath )
706✔
3338
{
3339
  QDir dir;
706✔
3340
  if ( !dir.exists( mDataDir ) )
706✔
3341
    dir.mkpath( mDataDir );
×
3342

3343
  QFileInfo newFile( filePath );
706✔
3344
  if ( !newFile.absoluteDir().exists() )
706✔
3345
  {
3346
    if ( !dir.mkpath( newFile.absolutePath() ) )
160✔
3347
    {
3348
      CoreUtils::log( "create path", QString( "Creating a folder failed for path: %1" ).arg( filePath ) );
×
3349
    }
3350
  }
3351
}
706✔
3352

3353
bool MerginApi::isInIgnore( const QFileInfo &info )
1,659✔
3354
{
3355
  return sIgnoreExtensions.contains( info.suffix() ) || sIgnoreFiles.contains( info.fileName() ) || info.filePath().contains( sMetadataFolder + "/" );
1,659✔
3356
}
3357

3358
bool MerginApi::excludeFromSync( const QString &filePath, const MerginConfig &config )
373✔
3359
{
3360
  if ( config.isValid && config.selectiveSyncEnabled )
373✔
3361
  {
3362
    QFileInfo info( filePath );
243✔
3363

3364
    bool isExcludedFormat = sIgnoreImageExtensions.contains( info.suffix().toLower() );
243✔
3365

3366
    if ( !isExcludedFormat )
243✔
3367
      return false;
20✔
3368

3369
    if ( config.selectiveSyncDir.isEmpty() )
223✔
3370
    {
3371
      return true; // we are ignoring photos in the entire project
96✔
3372
    }
3373
    else if ( filePath.startsWith( config.selectiveSyncDir ) )
127✔
3374
    {
3375
      return true; // we are ignoring photo in subfolder
119✔
3376
    }
3377
  }
243✔
3378
  return false;
138✔
3379
}
3380

3381
QSet<QString> MerginApi::listFiles( const QString &path )
585✔
3382
{
3383
  QSet<QString> files;
585✔
3384
  QDirIterator it( path, QStringList() << QStringLiteral( "*" ), QDir::Files, QDirIterator::Subdirectories );
1,755✔
3385
  while ( it.hasNext() )
2,244✔
3386
  {
3387
    it.next();
1,659✔
3388
    if ( !isInIgnore( it.fileInfo() ) )
1,659✔
3389
    {
3390
      files << it.filePath().replace( path, "" );
1,659✔
3391
    }
3392
  }
3393
  return files;
1,170✔
3394
}
585✔
3395

3396
void MerginApi::deleteAccount()
1✔
3397
{
3398
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
1✔
3399
  {
3400
    return;
×
3401
  }
3402

3403
  QNetworkRequest request = getDefaultRequest();
1✔
3404
  QUrl url( mApiRoot + QStringLiteral( "/v1/user" ) );
2✔
3405
  request.setUrl( url );
1✔
3406
  QNetworkReply *reply = mManager.deleteResource( request );
1✔
3407
  connect( reply, &QNetworkReply::finished, this, [this]() { this->deleteAccountFinished();} );
2✔
3408
  CoreUtils::log( "delete account " + mUserAuth->username(), QStringLiteral( "Requesting account deletion: " ) + url.toString() );
2✔
3409
}
1✔
3410

3411
void MerginApi::deleteAccountFinished()
1✔
3412
{
3413
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
1✔
3414
  Q_ASSERT( r );
1✔
3415

3416
  if ( r->error() == QNetworkReply::NoError )
1✔
3417
  {
3418
    CoreUtils::log( "delete account " + mUserAuth->username(), QStringLiteral( "Success" ) );
1✔
3419

3420
    // remove all local projects from the device
3421
    LocalProjectsList projects = mLocalProjects.projects();
1✔
3422
    for ( const LocalProject &info : projects )
36✔
3423
    {
3424
      mLocalProjects.removeLocalProject( info.id() );
35✔
3425
    }
3426

3427
    clearAuth();
1✔
3428

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

3445
    emit accountDeleted( false );
×
3446
  }
×
3447

3448
  r->deleteLater();
1✔
3449
}
1✔
3450

3451
void MerginApi::getServerConfig()
25✔
3452
{
3453
  QNetworkRequest request = getDefaultRequest();
25✔
3454
  QString urlString = mApiRoot + QStringLiteral( "/config" );
50✔
3455
  QUrl url( urlString );
25✔
3456
  request.setUrl( url );
25✔
3457

3458
  QNetworkReply *reply = mManager.get( request );
25✔
3459

3460
  connect( reply, &QNetworkReply::finished, this, &MerginApi::getServerConfigReplyFinished );
25✔
3461
  CoreUtils::log( "Config", QStringLiteral( "Requesting server configuration: " ) + url.toString() );
50✔
3462
}
25✔
3463

3464
void MerginApi::getServerConfigReplyFinished()
12✔
3465
{
3466
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
12✔
3467
  Q_ASSERT( r );
12✔
3468

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

3506
  r->deleteLater();
12✔
3507
}
12✔
3508

3509
MerginServerType::ServerType MerginApi::serverType() const
13✔
3510
{
3511
  return mServerType;
13✔
3512
}
3513

3514
void MerginApi::setServerType( const MerginServerType::ServerType &serverType )
17✔
3515
{
3516
  if ( mServerType != serverType )
17✔
3517
  {
3518
    if ( mServerType == MerginServerType::OLD && serverType == MerginServerType::SAAS )
6✔
3519
    {
3520
      emit serverWasUpgraded();
2✔
3521
    }
3522

3523
    mServerType = serverType;
6✔
3524
    QSettings settings;
6✔
3525
    settings.beginGroup( QStringLiteral( "Input/" ) );
6✔
3526
    settings.setValue( QStringLiteral( "serverType" ), mServerType );
12✔
3527
    settings.endGroup();
6✔
3528
    emit serverTypeChanged();
6✔
3529
    emit apiSupportsWorkspacesChanged();
6✔
3530
  }
6✔
3531
}
17✔
3532

3533
void MerginApi::listWorkspaces()
×
3534
{
3535
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
×
3536
  {
3537
    emit listWorkspacesFailed();
×
3538
    return;
×
3539
  }
3540

3541
  QUrl url( mApiRoot + QStringLiteral( "/v1/workspaces" ) );
×
3542
  QNetworkRequest request = getDefaultRequest( mUserAuth->hasAuthData() );
×
3543
  request.setUrl( url );
×
3544

3545
  QNetworkReply *reply = mManager.get( request );
×
3546
  CoreUtils::log( "list workspaces", QStringLiteral( "Requesting: " ) + url.toString() );
×
3547
  connect( reply, &QNetworkReply::finished, this, &MerginApi::listWorkspacesReplyFinished );
×
3548
}
×
3549

3550
void MerginApi::listWorkspacesReplyFinished()
×
3551
{
3552
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
×
3553
  Q_ASSERT( r );
×
3554

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

3569
      mUserInfo->setWorkspaces( workspaces );
×
3570
      emit listWorkspacesFinished( workspaces );
×
3571
    }
×
3572
    else
3573
    {
3574
      emit listWorkspacesFailed();
×
3575
    }
3576
  }
×
3577
  else
3578
  {
3579
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
3580
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listWorkspaces" ), r->errorString(), serverMsg );
×
3581
    CoreUtils::log( "list workspaces", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
3582
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: listWorkspaces" ) );
×
3583
    emit listWorkspacesFailed();
×
3584
  }
×
3585

3586
  r->deleteLater();
×
3587
}
×
3588

3589
void MerginApi::listInvitations()
×
3590
{
3591
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
×
3592
  {
3593
    emit listInvitationsFailed();
×
3594
    return;
×
3595
  }
3596

3597
  QUrl url( mApiRoot + QStringLiteral( "/v1/workspace/invitations" ) );
×
3598
  QNetworkRequest request = getDefaultRequest( mUserAuth->hasAuthData() );
×
3599
  request.setUrl( url );
×
3600

3601
  QNetworkReply *reply = mManager.get( request );
×
3602
  CoreUtils::log( "list invitations", QStringLiteral( "Requesting: " ) + url.toString() );
×
3603
  connect( reply, &QNetworkReply::finished, this, &MerginApi::listInvitationsReplyFinished );
×
3604
}
×
3605

3606
void MerginApi::listInvitationsReplyFinished()
×
3607
{
3608
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
×
3609
  Q_ASSERT( r );
×
3610

3611
  if ( r->error() == QNetworkReply::NoError )
×
3612
  {
3613
    CoreUtils::log( "list invitations", QStringLiteral( "Success" ) );
×
3614
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
×
3615
    if ( doc.isArray() )
×
3616
    {
3617
      QList<MerginInvitation> invitations;
×
3618
      QJsonArray array = doc.array();
×
3619
      for ( auto it = array.constBegin(); it != array.constEnd(); ++it )
×
3620
      {
3621
        MerginInvitation invite = MerginInvitation::fromJsonObject( it->toObject() );
×
3622
        invitations.append( invite );
×
3623
      }
×
3624

3625
      emit listInvitationsFinished( invitations );
×
3626
    }
×
3627
    else
3628
    {
3629
      emit listInvitationsFailed();
×
3630
    }
3631
  }
×
3632
  else
3633
  {
3634
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
3635
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listInvitations" ), r->errorString(), serverMsg );
×
3636
    CoreUtils::log( "list invitations", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
3637
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: listInvitations" ) );
×
3638
    emit listInvitationsFailed();
×
3639
  }
×
3640

3641
  r->deleteLater();
×
3642
}
×
3643

3644
void MerginApi::processInvitation( const QString &uuid, bool accept )
×
3645
{
3646
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
×
3647
  {
3648
    emit processInvitationFailed();
×
3649
    return;
×
3650
  }
3651

3652
  QNetworkRequest request = getDefaultRequest( true );
×
3653
  QString urlString = mApiRoot + QStringLiteral( "v1/workspace/invitation/%1" ).arg( uuid );
×
3654
  QUrl url( urlString );
×
3655
  request.setUrl( url );
×
3656
  request.setRawHeader( "Content-Type", "application/json" );
×
3657
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrAcceptFlag ), accept );
×
3658

3659
  QJsonDocument jsonDoc;
×
3660
  QJsonObject jsonObject;
×
3661
  jsonObject.insert( QStringLiteral( "accept" ), accept );
×
3662
  jsonDoc.setObject( jsonObject );
×
3663
  QByteArray json = jsonDoc.toJson( QJsonDocument::Compact );
×
3664
  QNetworkReply *reply = mManager.post( request, json );
×
3665
  CoreUtils::log( "process invitation", QStringLiteral( "Requesting: " ) + url.toString() );
×
3666
  connect( reply, &QNetworkReply::finished, this, &MerginApi::processInvitationReplyFinished );
×
3667
}
×
3668

3669
void MerginApi::processInvitationReplyFinished()
×
3670
{
3671
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
×
3672
  Q_ASSERT( r );
×
3673

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

3676
  if ( r->error() == QNetworkReply::NoError )
×
3677
  {
3678
    CoreUtils::log( "process invitation", QStringLiteral( "Success" ) );
×
3679
  }
3680
  else
3681
  {
3682
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
3683
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "processInvitation" ), r->errorString(), serverMsg );
×
3684
    CoreUtils::log( "process invitation", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
3685
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: processInvitation" ) );
×
3686
    emit processInvitationFailed();
×
3687
  }
×
3688

3689
  emit processInvitationFinished( accept );
×
3690

3691
  r->deleteLater();
×
3692
}
×
3693

3694
bool MerginApi::createWorkspace( const QString &workspaceName )
2✔
3695
{
3696
  if ( !validateAuth() )
2✔
3697
  {
3698
    emit missingAuthorizationError( workspaceName );
×
3699
    return false;
×
3700
  }
3701

3702
  if ( mApiVersionStatus != MerginApiStatus::OK )
2✔
3703
  {
3704
    return false;
×
3705
  }
3706

3707
  if ( !CoreUtils::isValidName( workspaceName ) )
2✔
3708
  {
3709
    emit notify( tr( "Workspace name contains invalid characters" ) );
×
3710
    return false;
×
3711
  }
3712

3713
  QNetworkRequest request = getDefaultRequest();
2✔
3714
  QUrl url( mApiRoot + QString( "/v1/workspace" ) );
4✔
3715
  request.setUrl( url );
2✔
3716
  request.setRawHeader( "Content-Type", "application/json" );
2✔
3717
  request.setRawHeader( "Accept", "application/json" );
2✔
3718
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrWorkspaceName ), workspaceName );
2✔
3719

3720
  QJsonDocument jsonDoc;
2✔
3721
  QJsonObject jsonObject;
2✔
3722
  jsonObject.insert( QStringLiteral( "name" ), workspaceName );
4✔
3723
  jsonDoc.setObject( jsonObject );
2✔
3724
  QByteArray json = jsonDoc.toJson( QJsonDocument::Compact );
2✔
3725

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

3730
  return true;
2✔
3731
}
2✔
3732

3733
void MerginApi::signOut()
×
3734
{
3735
  clearAuth();
×
3736
}
×
3737

3738
void MerginApi::refreshUserData()
×
3739
{
3740
  getUserInfo();
×
3741

3742
  if ( apiSupportsWorkspaces() )
×
3743
  {
3744
    getWorkspaceInfo();
×
3745
    // getServiceInfo is called automatically when workspace info finishes
3746
  }
3747
  else if ( mServerType == MerginServerType::OLD )
×
3748
  {
3749
    getServiceInfo();
×
3750
  }
3751
}
×
3752

3753
void MerginApi::createWorkspaceReplyFinished()
2✔
3754
{
3755
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
2✔
3756
  Q_ASSERT( r );
2✔
3757

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

3760
  if ( r->error() == QNetworkReply::NoError )
2✔
3761
  {
3762
    CoreUtils::log( "create " + workspaceName, QStringLiteral( "Success" ) );
2✔
3763
    emit workspaceCreated( workspaceName, true );
2✔
3764
  }
3765
  else
3766
  {
3767
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
3768
    QString message = QStringLiteral( "FAILED - %1: %2" ).arg( r->errorString(), serverMsg );
×
3769
    CoreUtils::log( "create " + workspaceName, message );
×
3770

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

3773
    if ( httpCode == 409 )
×
3774
    {
3775
      emit networkErrorOccurred( tr( "Workspace %1 already exists" ).arg( workspaceName ), QStringLiteral( "Mergin API error: createWorkspace" ), httpCode, workspaceName );
×
3776
    }
3777
    else
3778
    {
3779
      emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: createWorkspace" ), httpCode, workspaceName );
×
3780
    }
3781
    emit workspaceCreated( workspaceName, false );
×
3782
  }
×
3783
  r->deleteLater();
2✔
3784
}
2✔
3785

3786
bool MerginApi::apiSupportsWorkspaces()
1✔
3787
{
3788
  if ( mServerType == MerginServerType::SAAS || mServerType == MerginServerType::EE )
1✔
3789
  {
3790
    return true;
1✔
3791
  }
3792
  else
3793
  {
3794
    return false;
×
3795
  }
3796
}
3797

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