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

MerginMaps / input / 6274148729

22 Sep 2023 12:18PM UTC coverage: 61.904% (-0.1%) from 62.05%
6274148729

Pull #2797

github

PeterPetrik
fix code
Pull Request #2797: Profiler

7577 of 12240 relevant lines covered (61.9%)

102.31 hits per line

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

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

10
#include "merginapi.h"
11

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

22
#include "coreutils.h"
23
#include "geodiffutils.h"
24
#include "localprojectsmanager.h"
25
#include "../app/enumhelper.h"
26

27
#include <geodiff.h>
28

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

39

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

61
  qRegisterMetaType<Transactions>();
22✔
62

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

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

101
  getServerConfig();
22✔
102
  pingMergin();
22✔
103

104
  if ( mUserAuth->hasAuthData() )
22✔
105
  {
106
    QObject::connect( this, &MerginApi::pingMerginFinished, this, &MerginApi::getUserInfo, Qt::SingleShotConnection );
17✔
107
    QObject::connect( this, &MerginApi::userInfoReplyFinished, this, &MerginApi::getWorkspaceInfo, Qt::SingleShotConnection );
17✔
108
  }
109
}
22✔
110

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

117
  mServerType = static_cast<MerginServerType::ServerType>( serverType );
21✔
118

119
  mUserAuth->loadAuthData();
21✔
120
  mUserInfo->loadWorkspacesData();
21✔
121
}
21✔
122

123
MerginUserAuth *MerginApi::userAuth() const
69✔
124
{
125
  return mUserAuth;
69✔
126
}
127

128
MerginUserInfo *MerginApi::userInfo() const
15✔
129
{
130
  return mUserInfo;
15✔
131
}
132

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

138
MerginSubscriptionInfo *MerginApi::subscriptionInfo() const
122✔
139
{
140
  return mSubscriptionInfo;
122✔
141
}
142

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

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

153
  QUrlQuery query;
22✔
154

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

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

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

183
  query.addQueryItem( "order_params", QStringLiteral( "namespace_asc,name_asc" ) );
22✔
184

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

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

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

196
  QString requestId = CoreUtils::uuidWithoutBraces( QUuid::createUuid() );
22✔
197

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

202
  return requestId;
22✔
203
}
22✔
204

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

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

218
  // construct JSON body
219
  QJsonDocument body;
7✔
220
  QJsonObject projects;
7✔
221
  QJsonArray projectsArr = QJsonArray::fromStringList( projectNames );
7✔
222

223
  projects.insert( "projects", projectsArr );
7✔
224
  body.setObject( projects );
7✔
225

226
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/by_names" ) );
14✔
227

228
  QNetworkRequest request = getDefaultRequest( true );
7✔
229
  request.setUrl( url );
7✔
230
  request.setRawHeader( "Content-type", "application/json" );
7✔
231

232
  QString requestId = CoreUtils::uuidWithoutBraces( QUuid::createUuid() );
7✔
233

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

238
  return requestId;
7✔
239
}
7✔
240

241

242
void MerginApi::downloadNextItem( const QString &projectFullName )
275✔
243
{
244
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
275✔
245
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
275✔
246

247
  if ( transaction.downloadQueue.isEmpty() )
275✔
248
  {
249
    // there's nothing to download so just finalize the pull
250
    finalizeProjectPull( projectFullName );
123✔
251
    return;
123✔
252
  }
253

254
  DownloadQueueItem item = transaction.downloadQueue.takeFirst();
152✔
255

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

265
  QNetworkRequest request = getDefaultRequest();
152✔
266
  request.setUrl( url );
152✔
267
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
152✔
268
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrTempFileName ), item.tempFileName );
152✔
269

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

277
  Q_ASSERT( !transaction.replyPullItem );
152✔
278
  transaction.replyPullItem = mManager.get( request );
152✔
279
  connect( transaction.replyPullItem, &QNetworkReply::finished, this, &MerginApi::downloadItemReplyFinished );
152✔
280

281
  CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Requesting item: " ) + url.toString() +
304✔
282
                  ( !range.isEmpty() ? " Range: " + range : QString() ) );
304✔
283
}
152✔
284

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

290
  QString path = getTempProjectDir( getFullProjectName( projectNamespace, projectName ) );
124✔
291
  QDir( path ).removeRecursively();
62✔
292
}
62✔
293

294
QNetworkRequest MerginApi::getDefaultRequest( bool withAuth )
1,074✔
295
{
296
  QNetworkRequest request;
1,074✔
297
  QString info = CoreUtils::appInfo();
1,074✔
298
  request.setRawHeader( "User-Agent", QByteArray( info.toUtf8() ) );
1,074✔
299
  if ( withAuth )
1,074✔
300
    request.setRawHeader( "Authorization", QByteArray( "Bearer " + mUserAuth->authToken() ) );
1,035✔
301

302
  return request;
2,148✔
303
}
1,074✔
304

305
bool MerginApi::projectFileHasBeenUpdated( const ProjectDiff &diff )
147✔
306
{
307
  for ( QString filePath : diff.remoteAdded )
149✔
308
  {
309
    if ( CoreUtils::hasProjectFileExtension( filePath ) )
2✔
310
      return true;
×
311
  }
2✔
312

313
  for ( QString filePath : diff.remoteUpdated )
153✔
314
  {
315
    if ( CoreUtils::hasProjectFileExtension( filePath ) )
6✔
316
      return true;
×
317
  }
6✔
318

319
  return false;
147✔
320
}
321

322
bool MerginApi::supportsSelectiveSync() const
×
323
{
324
  return mSupportsSelectiveSync;
×
325
}
326

327
void MerginApi::setSupportsSelectiveSync( bool supportsSelectiveSync )
4✔
328
{
329
  mSupportsSelectiveSync = supportsSelectiveSync;
4✔
330
}
4✔
331

332
bool MerginApi::apiSupportsSubscriptions() const
31✔
333
{
334
  return mApiSupportsSubscriptions;
31✔
335
}
336

337
void MerginApi::setApiSupportsSubscriptions( bool apiSupportsSubscriptions )
11✔
338
{
339
  if ( mApiSupportsSubscriptions != apiSupportsSubscriptions )
11✔
340
  {
341
    mApiSupportsSubscriptions = apiSupportsSubscriptions;
10✔
342
    emit apiSupportsSubscriptionsChanged();
10✔
343
  }
344
}
11✔
345

346
#if !defined(USE_MERGIN_DUMMY_API_KEY)
347
#include "merginsecrets.cpp"
348
#endif
349

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

362
void MerginApi::downloadItemReplyFinished()
152✔
363
{
364
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
152✔
365
  Q_ASSERT( r );
152✔
366

367
  QString projectFullName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ) ).toString();
304✔
368
  QString tempFileName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrTempFileName ) ).toString();
304✔
369

370
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
152✔
371
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
152✔
372
  Q_ASSERT( r == transaction.replyPullItem );
152✔
373

374
  if ( r->error() == QNetworkReply::NoError )
152✔
375
  {
376
    QByteArray data = r->readAll();
151✔
377

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

380
    QString tempFolder = getTempProjectDir( projectFullName );
151✔
381
    QString tempFilePath = tempFolder + "/" + tempFileName;
151✔
382
    createPathIfNotExists( tempFilePath );
151✔
383

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

396
    transaction.transferedSize += data.size();
151✔
397
    emit syncProjectStatusChanged( projectFullName, transaction.transferedSize / transaction.totalSize );
151✔
398

399
    transaction.replyPullItem->deleteLater();
151✔
400
    transaction.replyPullItem = nullptr;
151✔
401

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

414
    transaction.replyPullItem->deleteLater();
1✔
415
    transaction.replyPullItem = nullptr;
1✔
416

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

420
    if ( transaction.firstTimeDownload )
1✔
421
    {
422
      Q_ASSERT( !transaction.projectDir.isEmpty() );
1✔
423
      QDir( transaction.projectDir ).removeRecursively();
1✔
424
    }
425

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

429
    finishProjectSync( projectFullName, false );
1✔
430
  }
1✔
431
}
152✔
432

433
void MerginApi::cacheServerConfig()
39✔
434
{
435
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
39✔
436
  Q_ASSERT( r );
39✔
437

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

440
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
39✔
441
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
39✔
442
  Q_ASSERT( r == transaction.replyPullItem );
39✔
443

444
  if ( r->error() == QNetworkReply::NoError )
39✔
445
  {
446
    QByteArray data = r->readAll();
39✔
447

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

451
    transaction.replyPullItem->deleteLater();
39✔
452
    transaction.replyPullItem = nullptr;
39✔
453

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

465
    transaction.replyPullItem->deleteLater();
×
466
    transaction.replyPullItem = nullptr;
×
467

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

471
    if ( transaction.firstTimeDownload )
×
472
    {
473
      CoreUtils::removeDir( transaction.projectDir );
×
474
    }
475

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

479
    finishProjectSync( projectFullName, false );
×
480
  }
×
481
}
39✔
482

483

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

491
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
152✔
492
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
152✔
493

494
  QString chunkID = file.chunks.at( chunkNo );
152✔
495

496
  QString filePath;
152✔
497
  if ( file.diffName.isEmpty() )
152✔
498
    filePath = transaction.projectDir + "/" + file.path;
140✔
499
  else  // use diff file instead of full file
500
    filePath = transaction.projectDir + "/.mergin/" + file.diffName;
12✔
501

502
  QFile f( filePath );
152✔
503
  QByteArray data;
152✔
504

505
  if ( f.open( QIODevice::ReadOnly ) )
152✔
506
  {
507
    f.seek( chunkNo * UPLOAD_CHUNK_SIZE );
152✔
508
    data = f.read( UPLOAD_CHUNK_SIZE );
152✔
509
  }
510

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

517
  Q_ASSERT( !transaction.replyPushFile );
152✔
518
  transaction.replyPushFile = mManager.post( request, data );
152✔
519
  connect( transaction.replyPushFile, &QNetworkReply::finished, this, &MerginApi::pushFileReplyFinished );
152✔
520

521
  CoreUtils::log( "push " + projectFullName, QStringLiteral( "Uploading item: " ) + url.toString() );
304✔
522
}
152✔
523

524
void MerginApi::pushStart( const QString &projectFullName, const QByteArray &json )
115✔
525
{
526
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
115✔
527
  {
528
    return;
×
529
  }
530

531
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
115✔
532
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
115✔
533

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

540
  Q_ASSERT( !transaction.replyPushStart );
115✔
541
  transaction.replyPushStart = mManager.post( request, json );
115✔
542
  connect( transaction.replyPushStart, &QNetworkReply::finished, this, &MerginApi::pushStartReplyFinished );
115✔
543

544
  CoreUtils::log( "push " + projectFullName, QStringLiteral( "Starting push request: " ) + url.toString() );
230✔
545
}
115✔
546

547
void MerginApi::cancelPush( const QString &projectFullName )
2✔
548
{
549
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
2✔
550
  {
551
    return;
×
552
  }
553

554
  if ( !mTransactionalStatus.contains( projectFullName ) )
2✔
555
    return;
×
556

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

559
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
2✔
560

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

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

587
    sendPushCancelRequest( projectFullName, transactionUUID );
×
588
  }
×
589
  else
590
  {
591
    Q_ASSERT( false );  // unexpected state
×
592
  }
593
}
594

595

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

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

609
void MerginApi::cancelPull( const QString &projectFullName )
2✔
610
{
611
  if ( !mTransactionalStatus.contains( projectFullName ) )
2✔
612
    return;
×
613

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

616
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
2✔
617

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

636
void MerginApi::pushFinish( const QString &projectFullName, const QString &transactionUUID )
110✔
637
{
638
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
110✔
639
  {
640
    return;
×
641
  }
642

643
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
110✔
644
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
110✔
645

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

652
  Q_ASSERT( !transaction.replyPushFinish );
110✔
653
  transaction.replyPushFinish = mManager.post( request, QByteArray() );
110✔
654
  connect( transaction.replyPushFinish, &QNetworkReply::finished, this, &MerginApi::pushFinishReplyFinished );
110✔
655

656
  CoreUtils::log( "push " + projectFullName, QStringLiteral( "Requesting transaction finish: " ) + transactionUUID );
110✔
657
}
110✔
658

659
bool MerginApi::pullProject( const QString &projectNamespace, const QString &projectName, bool withAuth )
100✔
660
{
661
  QString projectFullName = getFullProjectName( projectNamespace, projectName );
200✔
662
  bool pullHasStarted = false;
100✔
663

664
  CoreUtils::log( "pull " + projectFullName, "### Starting ###" );
100✔
665

666
  QNetworkReply *reply = getProjectInfo( projectFullName, withAuth );
100✔
667
  if ( reply )
100✔
668
  {
669
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Requesting project info: " ) + reply->request().url().toString() );
200✔
670

671
    Q_ASSERT( !mTransactionalStatus.contains( projectFullName ) );
100✔
672
    mTransactionalStatus.insert( projectFullName, TransactionStatus() );
100✔
673
    mTransactionalStatus[projectFullName].replyPullProjectInfo = reply;
100✔
674
    mTransactionalStatus[projectFullName].configAllowed = mSupportsSelectiveSync;
100✔
675
    mTransactionalStatus[projectFullName].type = TransactionStatus::Pull;
100✔
676

677
    emit syncProjectStatusChanged( projectFullName, 0 );
100✔
678

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

687
  return pullHasStarted;
100✔
688
}
100✔
689

690
bool MerginApi::pushProject( const QString &projectNamespace, const QString &projectName, bool isInitialPush )
143✔
691
{
692
  QString projectFullName = getFullProjectName( projectNamespace, projectName );
286✔
693
  bool pushHasStarted = false;
143✔
694

695
  CoreUtils::log( "push " + projectFullName, "### Starting ###" );
143✔
696

697
  QNetworkReply *reply = getProjectInfo( projectFullName );
143✔
698
  if ( reply )
143✔
699
  {
700
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Requesting project info: " ) + reply->request().url().toString() );
286✔
701

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

710
    emit syncProjectStatusChanged( projectFullName, 0 );
143✔
711

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

720
  return pushHasStarted;
143✔
721
}
143✔
722

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

732
  mUserAuth->blockSignals( true );
8✔
733
  mUserAuth->setPassword( password );
8✔
734
  mUserAuth->blockSignals( false );
8✔
735

736
  QNetworkRequest request = getDefaultRequest( false );
8✔
737
  QString urlString = mApiRoot + QStringLiteral( "v1/auth/login" );
16✔
738
  QUrl url( urlString );
8✔
739
  request.setUrl( url );
8✔
740
  request.setRawHeader( "Content-Type", "application/json" );
8✔
741

742
  QJsonDocument jsonDoc;
8✔
743
  QJsonObject jsonObject;
8✔
744
  jsonObject.insert( QStringLiteral( "login" ), login );
16✔
745
  jsonObject.insert( QStringLiteral( "password" ), mUserAuth->password() );
16✔
746
  jsonDoc.setObject( jsonObject );
8✔
747
  QByteArray json = jsonDoc.toJson( QJsonDocument::Compact );
8✔
748

749
  QNetworkReply *reply = mManager.post( request, json );
8✔
750
  connect( reply, &QNetworkReply::finished, this, &MerginApi::authorizeFinished );
8✔
751
  CoreUtils::log( "auth", QStringLiteral( "Requesting authorization: " ) + url.toString() );
16✔
752
}
8✔
753

754
void MerginApi::registerUser( const QString &username,
8✔
755
                              const QString &email,
756
                              const QString &password,
757
                              const QString &confirmPassword,
758
                              bool acceptedTOC )
759
{
760
  // Some very basic checks, so we do not validate everything
761
  if ( username.isEmpty() || username.length() < 4 )
8✔
762
  {
763
    QString msg = tr( "Username must have at least 4 characters" );
1✔
764
    emit registrationFailed( msg, RegistrationError::RegistrationErrorType::USERNAME );
1✔
765
    return;
1✔
766
  }
1✔
767

768
  if ( !CoreUtils::isValidName( username ) )
7✔
769
  {
770
    QString msg = tr( "Username contains invalid characters" );
×
771
    emit registrationFailed( msg, RegistrationError::RegistrationErrorType::USERNAME );
×
772
    return;
×
773
  }
×
774

775
  if ( email.isEmpty() || !email.contains( '@' ) || !email.contains( '.' ) )
7✔
776
  {
777
    QString msg = tr( "Please enter a valid email" );
1✔
778
    emit registrationFailed( msg, RegistrationError::RegistrationErrorType::EMAIL );
1✔
779
    return;
1✔
780
  }
1✔
781

782
  if ( password.isEmpty() || password.length() < 8 )
6✔
783
  {
784
    QString msg = tr( "Password not strong enough. It must"
1✔
785
                      "%1 be at least 8 characters long"
786
                      "%1 contain lowercase characters"
787
                      "%1 contain uppercase characters"
788
                      "%1 contain digits or special characters" )
789
                  .arg( "<br />  -" );
2✔
790
    emit registrationFailed( msg, RegistrationError::RegistrationErrorType::PASSWORD );
1✔
791
    return;
1✔
792

793
  }
1✔
794

795
  if ( confirmPassword != password )
5✔
796
  {
797
    QString msg = tr( "Passwords do not match" );
1✔
798
    emit registrationFailed( msg, RegistrationError::RegistrationErrorType::CONFIRM_PASSWORD );
1✔
799
    return;
1✔
800
  }
1✔
801

802
  if ( !acceptedTOC )
4✔
803
  {
804
    QString msg = tr( "Please accept Terms and Privacy Policy" );
1✔
805
    emit registrationFailed( msg, RegistrationError::RegistrationErrorType::TOC );
1✔
806
    return;
1✔
807
  }
1✔
808

809
  // request
810
  QNetworkRequest request = getDefaultRequest( false );
3✔
811
  QString urlString = mApiRoot + QStringLiteral( "v1/auth/register" );
6✔
812
  QUrl url( urlString );
3✔
813
  request.setUrl( url );
3✔
814
  request.setRawHeader( "Content-Type", "application/json" );
3✔
815

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

829
void MerginApi::getUserInfo()
20✔
830
{
831
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
20✔
832
  {
833
    return;
1✔
834
  }
835

836
  QString urlString;
19✔
837
  if ( mServerType == MerginServerType::OLD )
19✔
838
  {
839
    urlString = mApiRoot + QStringLiteral( "v1/user/%1" ).arg( mUserAuth->username() );
×
840
  }
841
  else
842
  {
843
    urlString = mApiRoot + QStringLiteral( "v1/user/profile" );
19✔
844
  }
845

846
  QNetworkRequest request = getDefaultRequest();
19✔
847
  QUrl url( urlString );
19✔
848
  request.setUrl( url );
19✔
849

850
  QNetworkReply *reply = mManager.get( request );
19✔
851
  CoreUtils::log( "user info", QStringLiteral( "Requesting user info: " ) + url.toString() );
38✔
852
  connect( reply, &QNetworkReply::finished, this, &MerginApi::getUserInfoFinished );
19✔
853
}
19✔
854

855
void MerginApi::getWorkspaceInfo()
25✔
856
{
857
  if ( mServerType == MerginServerType::OLD )
25✔
858
  {
859
    return;
×
860
  }
861

862
  if ( mUserInfo->activeWorkspaceId() == -1 )
25✔
863
  {
864
    return;
×
865
  }
866

867
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
25✔
868
  {
869
    return;
×
870
  }
871

872
  QString urlString = mApiRoot + QStringLiteral( "v1/workspace/%1" ).arg( mUserInfo->activeWorkspaceId() );
50✔
873
  QNetworkRequest request = getDefaultRequest();
25✔
874
  QUrl url( urlString );
25✔
875
  request.setUrl( url );
25✔
876

877
  QNetworkReply *reply = mManager.get( request );
25✔
878
  CoreUtils::log( "workspace info", QStringLiteral( "Requesting workspace info: " ) + url.toString() );
50✔
879
  connect( reply, &QNetworkReply::finished, this, &MerginApi::getWorkspaceInfoReplyFinished );
25✔
880
}
25✔
881

882
void MerginApi::getServiceInfo()
34✔
883
{
884
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
34✔
885
  {
886
    return;
×
887
  }
888

889
  QString urlString;
34✔
890

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

904
  QNetworkRequest request = getDefaultRequest( true );
34✔
905
  QUrl url( urlString );
34✔
906
  request.setUrl( url );
34✔
907

908
  QNetworkReply *reply = mManager.get( request );
34✔
909

910
  connect( reply, &QNetworkReply::finished, this, &MerginApi::getServiceInfoReplyFinished );
34✔
911

912
  CoreUtils::log( "Service info", QStringLiteral( "Requesting service info: " ) + url.toString() );
68✔
913
}
34✔
914

915
void MerginApi::getServiceInfoReplyFinished()
33✔
916
{
917
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
33✔
918
  Q_ASSERT( r );
33✔
919

920
  if ( r->error() == QNetworkReply::NoError )
33✔
921
  {
922
    CoreUtils::log( "Service info", QStringLiteral( "Success" ) );
33✔
923

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

937
    mSubscriptionInfo->clear();
×
938

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

954
  r->deleteLater();
33✔
955
}
33✔
956

957
void MerginApi::clearAuth()
3✔
958
{
959
  mUserAuth->clear();
3✔
960
  mUserInfo->clear();
3✔
961
  mUserInfo->clearCachedWorkspacesInfo();
3✔
962
  mWorkspaceInfo->clear();
3✔
963
  mSubscriptionInfo->clear();
3✔
964
}
3✔
965

966
void MerginApi::resetApiRoot()
×
967
{
968
  QSettings settings;
×
969
  settings.beginGroup( QStringLiteral( "Input/" ) );
×
970
  setApiRoot( defaultApiRoot() );
×
971
  settings.endGroup();
×
972
}
×
973

974
QString MerginApi::resetPasswordUrl()
×
975
{
976
  if ( !mApiRoot.isEmpty() )
×
977
  {
978
    QUrl base( mApiRoot );
×
979
    return base.resolved( QUrl( "login/reset" ) ).toString();
×
980
  }
×
981
  return QString();
×
982
}
983

984
bool MerginApi::createProject( const QString &projectNamespace, const QString &projectName, bool isPublic )
40✔
985
{
986
  if ( !validateAuth() )
40✔
987
  {
988
    emit missingAuthorizationError( projectName );
×
989
    return false;
×
990
  }
991

992
  if ( mApiVersionStatus != MerginApiStatus::OK )
40✔
993
  {
994
    return false;
×
995
  }
996

997
  QString projectFullName = getFullProjectName( projectNamespace, projectName );
80✔
998

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

1006
  QJsonDocument jsonDoc;
40✔
1007
  QJsonObject jsonObject;
40✔
1008
  jsonObject.insert( QStringLiteral( "name" ), projectName );
80✔
1009
  jsonObject.insert( QStringLiteral( "public" ), isPublic );
80✔
1010
  jsonDoc.setObject( jsonObject );
40✔
1011
  QByteArray json = jsonDoc.toJson( QJsonDocument::Compact );
40✔
1012

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

1017
  return true;
40✔
1018
}
40✔
1019

1020
void MerginApi::deleteProject( const QString &projectNamespace, const QString &projectName, bool informUser )
43✔
1021
{
1022
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
43✔
1023
  {
1024
    return;
1✔
1025
  }
1026

1027
  QString projectFullName = getFullProjectName( projectNamespace, projectName );
84✔
1028

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

1038
void MerginApi::saveAuthData()
12✔
1039
{
1040
  QSettings settings;
12✔
1041
  settings.beginGroup( "Input/" );
12✔
1042
  settings.setValue( "apiRoot", mApiRoot );
12✔
1043
  settings.endGroup();
12✔
1044

1045
  mUserAuth->saveAuthData();
12✔
1046
  mUserInfo->clear();
12✔
1047
}
12✔
1048

1049
void MerginApi::createProjectFinished()
40✔
1050
{
1051
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
40✔
1052
  Q_ASSERT( r );
40✔
1053

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

1056
  QString projectNamespace, projectName;
40✔
1057
  extractProjectName( projectFullName, projectNamespace, projectName );
40✔
1058

1059
  if ( r->error() == QNetworkReply::NoError )
40✔
1060
  {
1061
    CoreUtils::log( "create " + projectFullName, QStringLiteral( "Success" ) );
39✔
1062
    emit projectCreated( projectFullName, true );
39✔
1063

1064

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

1073
        QDir projectDir( info.projectDir );
3✔
1074
        if ( projectDir.exists() && !projectDir.isEmpty() )
3✔
1075
        {
1076
          pushProject( projectNamespace, projectName, true );
3✔
1077
        }
1078
      }
3✔
1079
    }
39✔
1080
  }
1081
  else
1082
  {
1083
    QByteArray data = r->readAll();
1✔
1084
    QString code = extractServerErrorCode( data );
1✔
1085
    QString serverMsg = extractServerErrorMsg( data );
1✔
1086
    QString message = QStringLiteral( "FAILED - %1: %2" ).arg( r->errorString(), serverMsg );
2✔
1087
    bool showLimitReachedDialog = EnumHelper::isEqual( code, ErrorCode::ProjectsLimitHit );
1✔
1088

1089
    CoreUtils::log( "create " + projectFullName, message );
1✔
1090

1091
    emit projectCreated( projectFullName, false );
1✔
1092

1093
    if ( showLimitReachedDialog )
1✔
1094
    {
1095
      int maxProjects = 0;
×
1096
      QVariant maxProjectVariant = extractServerErrorValue( data, "projects_quota" );
×
1097
      if ( maxProjectVariant.isValid() )
×
1098
        maxProjects = maxProjectVariant.toInt();
×
1099
      emit projectLimitReached( maxProjects, serverMsg );
×
1100
    }
×
1101
    else
1102
    {
1103
      int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
1✔
1104
      emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: createProject" ), httpCode, projectName );
1✔
1105
    }
1106
  }
1✔
1107
  r->deleteLater();
40✔
1108
}
40✔
1109

1110
void MerginApi::deleteProjectFinished( bool informUser )
42✔
1111
{
1112
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
42✔
1113
  Q_ASSERT( r );
42✔
1114

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

1117
  if ( r->error() == QNetworkReply::NoError )
42✔
1118
  {
1119
    CoreUtils::log( "delete " + projectFullName, QStringLiteral( "Success" ) );
2✔
1120

1121
    if ( informUser )
2✔
1122
      emit notify( QStringLiteral( "Project deleted" ) );
2✔
1123

1124
    emit serverProjectDeleted( projectFullName, true );
2✔
1125
  }
1126
  else
1127
  {
1128
    QString serverMsg = extractServerErrorMsg( r->readAll() );
80✔
1129
    CoreUtils::log( "delete " + projectFullName, QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) );
40✔
1130
    emit serverProjectDeleted( projectFullName, false );
40✔
1131
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: deleteProject" ) );
80✔
1132
  }
40✔
1133
  r->deleteLater();
42✔
1134
}
42✔
1135

1136
void MerginApi::authorizeFinished()
8✔
1137
{
1138
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
8✔
1139
  Q_ASSERT( r );
8✔
1140

1141
  if ( r->error() == QNetworkReply::NoError )
8✔
1142
  {
1143
    CoreUtils::log( "auth", QStringLiteral( "Success" ) );
8✔
1144
    const QByteArray data = r->readAll();
8✔
1145
    QJsonDocument doc = QJsonDocument::fromJson( data );
8✔
1146
    if ( doc.isObject() )
8✔
1147
    {
1148
      QJsonObject docObj = doc.object();
8✔
1149
      mUserAuth->setFromJson( docObj );
8✔
1150
    }
8✔
1151
    else
1152
    {
1153
      // keep username and password, but clear token
1154
      // this is problem with internet connection or server
1155
      // so do not force user to input login credentials again
1156
      mUserAuth->clearTokenData();
×
1157
      emit authFailed();
×
1158
      CoreUtils::log( "Auth", QStringLiteral( "FAILED - invalid JSON response" ) );
×
1159
      emit notify( "Internal server error during authorization" );
×
1160
    }
1161
  }
8✔
1162
  else
1163
  {
1164
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
1165
    QVariant statusCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute );
×
1166
    int status = statusCode.toInt();
×
1167
    CoreUtils::log( "Auth", QStringLiteral( "FAILED - %1. %2 (%3)" ).arg( r->errorString(), serverMsg, QString::number( status ) ) );
×
1168

1169
    if ( status == 401 )
×
1170
    {
1171
      // OK, we have INVALID username or password or
1172
      // our user got blocked on the server by admin or owner
1173
      // lets show error to user and let him try different credentials
1174
      emit authFailed();
×
1175
      emit notify( serverMsg );
×
1176

1177
      mUserAuth->blockSignals( true );
×
1178
      mUserAuth->setUsername( QString() );
×
1179
      mUserAuth->setPassword( QString() );
×
1180
      mUserAuth->blockSignals( false );
×
1181

1182
    }
1183
    else
1184
    {
1185
      // keep username and password
1186
      // this is problem with internet connection or server
1187
      // so do not force user to input login credentials again
1188
      emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: authorize" ) );
×
1189
    }
1190

1191
    // in case of any error, just clean token and request new one
1192
    mUserAuth->clearTokenData();
×
1193
  }
×
1194

1195
  if ( mAuthLoopEvent.isRunning() )
8✔
1196
  {
1197
    mAuthLoopEvent.exit();
1✔
1198
  }
1199
  r->deleteLater();
8✔
1200
}
8✔
1201

1202
void MerginApi::registrationFinished( const QString &username, const QString &password )
3✔
1203
{
1204
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
3✔
1205
  Q_ASSERT( r );
3✔
1206

1207
  if ( r->error() == QNetworkReply::NoError )
3✔
1208
  {
1209
    CoreUtils::log( "register", QStringLiteral( "Success" ) );
3✔
1210
    QString msg = tr( "Registration successful" );
3✔
1211
    emit notify( msg );
3✔
1212

1213
    if ( !username.isEmpty() && !password.isEmpty() ) // log in immediately
3✔
1214
      authorize( username, password );
3✔
1215

1216
    emit registrationSucceeded();
3✔
1217
  }
3✔
1218
  else
1219
  {
1220
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
1221
    CoreUtils::log( "register", QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) );
×
1222
    QVariant statusCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute );
×
1223
    int status = statusCode.toInt();
×
1224
    if ( status == 401 || status == 400 )
×
1225
    {
1226
      emit registrationFailed( serverMsg, RegistrationError::RegistrationErrorType::OTHER );
×
1227
      emit notify( serverMsg );
×
1228
    }
1229
    else if ( status == 404 )
×
1230
    {
1231
      // the self-registration is not allowed on the server
1232
      QString msg = tr( "New registrations are not allowed on the selected Mergin server.%1Please check with your administrator." ).arg( "\n" );
×
1233
      emit registrationFailed( msg, RegistrationError::RegistrationErrorType::OTHER );
×
1234
      emit notify( msg );
×
1235
    }
×
1236
    else
1237
    {
1238
      QString msg = QStringLiteral( "Mergin API error: register" );
×
1239
      emit registrationFailed( msg, RegistrationError::RegistrationErrorType::OTHER );
×
1240
      emit networkErrorOccurred( serverMsg, msg );
×
1241
    }
×
1242
  }
×
1243
  r->deleteLater();
3✔
1244
}
3✔
1245

1246
void MerginApi::pingMerginReplyFinished()
11✔
1247
{
1248
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
11✔
1249
  Q_ASSERT( r );
11✔
1250
  QString apiVersion;
11✔
1251
  QString serverMsg;
11✔
1252
  bool serverSupportsSubscriptions = false;
11✔
1253

1254
  if ( r->error() == QNetworkReply::NoError )
11✔
1255
  {
1256
    CoreUtils::log( "ping", QStringLiteral( "Success" ) );
11✔
1257
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
11✔
1258
    if ( doc.isObject() )
11✔
1259
    {
1260
      QJsonObject obj = doc.object();
11✔
1261
      apiVersion = obj.value( QStringLiteral( "version" ) ).toString();
11✔
1262
      serverSupportsSubscriptions = obj.value( QStringLiteral( "subscriptions_enabled" ) ).toBool();
11✔
1263
    }
11✔
1264
  }
11✔
1265
  else
1266
  {
1267
    serverMsg = extractServerErrorMsg( r->readAll() );
×
1268
    CoreUtils::log( "ping", QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) );
×
1269
  }
1270
  r->deleteLater();
11✔
1271
  emit pingMerginFinished( apiVersion, serverSupportsSubscriptions, serverMsg );
11✔
1272
}
11✔
1273

1274
void MerginApi::onPlanProductIdChanged()
15✔
1275
{
1276
  if ( mUserAuth->hasAuthData() )
15✔
1277
  {
1278
    if ( mServerType == MerginServerType::OLD )
14✔
1279
    {
1280
      getUserInfo();
×
1281
    }
1282
    else
1283
    {
1284
      getWorkspaceInfo();
14✔
1285
    }
1286
  }
1287
}
15✔
1288

1289
QNetworkReply *MerginApi::getProjectInfo( const QString &projectFullName, bool withAuth )
243✔
1290
{
1291
  if ( withAuth && !validateAuth() )
243✔
1292
  {
1293
    emit missingAuthorizationError( projectFullName );
×
1294
    return nullptr;
×
1295
  }
1296

1297
  if ( mApiVersionStatus != MerginApiStatus::OK )
243✔
1298
  {
1299
    return nullptr;
×
1300
  }
1301

1302
  int sinceVersion = -1;
243✔
1303
  LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName );
243✔
1304
  if ( projectInfo.isValid() )
243✔
1305
  {
1306
    // let's also fetch the recent history of diffable files
1307
    // (the "since" is inclusive, so if we are on v2, we want to use since=v3 which will include v2->v3, v3->v4, ...)
1308
    sinceVersion = projectInfo.localVersion + 1;
180✔
1309
  }
1310

1311
  QUrlQuery query;
243✔
1312
  if ( sinceVersion != -1 )
243✔
1313
    query.addQueryItem( QStringLiteral( "since" ), QStringLiteral( "v%1" ).arg( sinceVersion ) );
360✔
1314

1315
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/%1" ).arg( projectFullName ) );
486✔
1316
  url.setQuery( query );
243✔
1317

1318
  QNetworkRequest request = getDefaultRequest( withAuth );
243✔
1319
  request.setUrl( url );
243✔
1320
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
243✔
1321

1322
  return mManager.get( request );
243✔
1323
}
243✔
1324

1325
bool MerginApi::validateAuth()
816✔
1326
{
1327
  if ( !mUserAuth->hasAuthData() )
816✔
1328
  {
1329
    emit authRequested();
×
1330
    return false;
×
1331
  }
1332

1333
  if ( mUserAuth->authToken().isEmpty() || mUserAuth->tokenExpiration() < QDateTime().currentDateTime().toUTC() )
816✔
1334
  {
1335
    authorize( mUserAuth->username(), mUserAuth->password() );
1✔
1336
    CoreUtils::log( QStringLiteral( "MerginApi" ), QStringLiteral( "Requesting authorization because of missing or expired token." ) );
2✔
1337
    mAuthLoopEvent.exec();
1✔
1338
  }
1339
  return true;
816✔
1340
}
1341

1342
void MerginApi::checkMerginVersion( QString apiVersion, bool serverSupportsSubscriptions, QString msg )
11✔
1343
{
1344
  setApiSupportsSubscriptions( serverSupportsSubscriptions );
11✔
1345

1346
  if ( msg.isEmpty() )
11✔
1347
  {
1348
    int major = -1;
11✔
1349
    int minor = -1;
11✔
1350
    QRegularExpression re;
11✔
1351
    re.setPattern( QStringLiteral( "(?<major>\\d+)[.](?<minor>\\d+)" ) );
11✔
1352
    QRegularExpressionMatch match = re.match( apiVersion );
11✔
1353
    if ( match.hasMatch() )
11✔
1354
    {
1355
      major = match.captured( "major" ).toInt();
11✔
1356
      minor = match.captured( "minor" ).toInt();
11✔
1357
    }
1358

1359
    if ( ( MERGIN_API_VERSION_MAJOR == major && MERGIN_API_VERSION_MINOR <= minor ) || ( MERGIN_API_VERSION_MAJOR < major ) )
11✔
1360
    {
1361
      setApiVersionStatus( MerginApiStatus::OK );
11✔
1362
    }
1363
    else
1364
    {
1365
      setApiVersionStatus( MerginApiStatus::INCOMPATIBLE );
×
1366
    }
1367
  }
11✔
1368
  else
1369
  {
1370
    setApiVersionStatus( MerginApiStatus::NOT_FOUND );
×
1371
  }
1372
}
11✔
1373

1374
bool MerginApi::extractProjectName( const QString &sourceString, QString &projectNamespace, QString &name )
189✔
1375
{
1376
  QStringList parts = sourceString.split( "/" );
189✔
1377
  if ( parts.length() > 1 )
189✔
1378
  {
1379
    projectNamespace = parts.at( parts.length() - 2 );
189✔
1380
    name = parts.last();
189✔
1381
    return true;
189✔
1382
  }
1383
  else
1384
  {
1385
    name = sourceString;
×
1386
    return false;
×
1387
  }
1388
}
189✔
1389

1390
QString MerginApi::extractServerErrorCode( const QByteArray &data )
1✔
1391
{
1392
  QVariant code = extractServerErrorValue( data, QStringLiteral( "code" ) );
2✔
1393
  if ( code.isValid() )
1✔
1394
    return code.toString();
×
1395
  return QString();
1✔
1396
}
1✔
1397

1398
QVariant MerginApi::extractServerErrorValue( const QByteArray &data, const QString &key )
1✔
1399
{
1400
  QJsonDocument doc = QJsonDocument::fromJson( data );
1✔
1401
  if ( doc.isObject() )
1✔
1402
  {
1403
    QJsonObject obj = doc.object();
1✔
1404
    if ( obj.contains( key ) )
1✔
1405
    {
1406
      QJsonValue val = obj.value( key );
×
1407
      return val.toVariant();
×
1408
    }
×
1409
  }
1✔
1410

1411
  return QVariant();
1✔
1412
}
1✔
1413

1414
QString MerginApi::extractServerErrorMsg( const QByteArray &data )
49✔
1415
{
1416
  QString serverMsg = "[can't parse server error]";
49✔
1417
  QJsonDocument doc = QJsonDocument::fromJson( data );
49✔
1418
  if ( doc.isObject() )
49✔
1419
  {
1420
    QJsonObject obj = doc.object();
44✔
1421
    if ( obj.contains( QStringLiteral( "detail" ) ) )
44✔
1422
    {
1423
      QJsonValue vDetail = obj.value( "detail" );
42✔
1424
      if ( vDetail.isString() )
42✔
1425
      {
1426
        serverMsg = vDetail.toString();
42✔
1427
      }
1428
      else if ( vDetail.isObject() )
×
1429
      {
1430
        serverMsg = QJsonDocument( vDetail.toObject() ).toJson();
×
1431
      }
1432
    }
42✔
1433
    else if ( obj.contains( QStringLiteral( "name" ) ) )
2✔
1434
    {
1435
      QJsonValue val = obj.value( "name" );
2✔
1436
      if ( val.isArray() )
2✔
1437
      {
1438
        QJsonArray errors = val.toArray();
1✔
1439
        QStringList messages;
1✔
1440
        for ( auto it = errors.constBegin(); it != errors.constEnd(); ++it )
2✔
1441
        {
1442
          messages << it->toString();
1✔
1443
        }
1444
        serverMsg = messages.join( " " );
1✔
1445
      }
1✔
1446
    }
2✔
1447
    else
1448
    {
1449
      serverMsg = "[can't parse server error]";
×
1450
    }
1451
  }
44✔
1452
  else
1453
  {
1454
    // take only first 1000 bytes of the message ~ there are situations when data is an unclosed string that would eat the whole log memory
1455
    serverMsg = data.mid( 0, 1000 );
5✔
1456
  }
1457

1458
  return serverMsg;
98✔
1459
}
49✔
1460

1461

1462
LocalProject MerginApi::getLocalProject( const QString &projectFullName )
7✔
1463
{
1464
  return mLocalProjects.projectFromMerginName( projectFullName );
7✔
1465
}
1466

1467
ProjectDiff MerginApi::localProjectChanges( const QString &projectDir )
41✔
1468
{
1469
  MerginProjectMetadata projectMetadata = MerginProjectMetadata::fromCachedJson( projectDir + "/" + sMetadataFile );
82✔
1470
  QList<MerginFile> localFiles = getLocalProjectFiles( projectDir + "/" );
41✔
1471

1472
  MerginConfig config = MerginConfig::fromFile( projectDir + "/" + sMerginConfigFile );
82✔
1473

1474
  return compareProjectFiles( projectMetadata.files, projectMetadata.files, localFiles, projectDir, config.isValid, config );
82✔
1475
}
41✔
1476

1477
QString MerginApi::getTempProjectDir( const QString &projectFullName )
337✔
1478
{
1479
  return mDataDir + "/" + TEMP_FOLDER + projectFullName;
674✔
1480
}
1481

1482
QString MerginApi::getFullProjectName( QString projectNamespace, QString projectName ) // TODO: move to inpututils?
43,965✔
1483
{
1484
  return QString( "%1/%2" ).arg( projectNamespace ).arg( projectName );
43,965✔
1485
}
1486

1487
MerginApiStatus::VersionStatus MerginApi::apiVersionStatus() const
38✔
1488
{
1489
  return mApiVersionStatus;
38✔
1490
}
1491

1492
void MerginApi::setApiVersionStatus( const MerginApiStatus::VersionStatus &apiVersionStatus )
34✔
1493
{
1494
  if ( mApiVersionStatus != apiVersionStatus )
34✔
1495
  {
1496
    mApiVersionStatus = apiVersionStatus;
32✔
1497
    emit apiVersionStatusChanged();
32✔
1498
  }
1499
}
34✔
1500

1501
void MerginApi::pingMergin()
23✔
1502
{
1503
  if ( mApiVersionStatus == MerginApiStatus::OK ) return;
23✔
1504

1505
  setApiVersionStatus( MerginApiStatus::PENDING );
23✔
1506

1507
  QNetworkRequest request = getDefaultRequest( false );
23✔
1508
  QUrl url( mApiRoot + QStringLiteral( "/ping" ) );
46✔
1509
  request.setUrl( url );
23✔
1510

1511
  QNetworkReply *reply = mManager.get( request );
23✔
1512
  CoreUtils::log( "ping", QStringLiteral( "Requesting: " ) + url.toString() );
46✔
1513
  connect( reply, &QNetworkReply::finished, this, &MerginApi::pingMerginReplyFinished );
23✔
1514
}
23✔
1515

1516
void MerginApi::migrateProjectToMergin( const QString &projectName, const QString &projectNamespace )
3✔
1517
{
1518
  CoreUtils::log( "migrate project", projectName );
3✔
1519
  if ( projectNamespace.isEmpty() )
3✔
1520
  {
1521
    createProject( mUserAuth->username(), projectName );
3✔
1522
  }
1523
  else
1524
  {
1525
    createProject( projectNamespace, projectName );
×
1526
  }
1527
}
3✔
1528

1529
void MerginApi::detachProjectFromMergin( const QString &projectNamespace, const QString &projectName, bool informUser )
1✔
1530
{
1531
  // Remove mergin folder
1532
  QString projectFullName = getFullProjectName( projectNamespace, projectName );
2✔
1533
  LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName );
1✔
1534

1535
  if ( projectInfo.isValid() )
1✔
1536
  {
1537
    CoreUtils::removeDir( projectInfo.projectDir + "/.mergin" );
1✔
1538
  }
1539

1540
  // Update localProject
1541
  mLocalProjects.updateNamespace( projectInfo.projectDir, "" );
1✔
1542
  mLocalProjects.updateLocalVersion( projectInfo.projectDir, -1 );
1✔
1543

1544
  if ( informUser )
1✔
1545
    emit notify( tr( "Project detached from Mergin" ) );
1✔
1546

1547
  emit projectDetached( projectFullName );
1✔
1548
}
1✔
1549

1550
QString MerginApi::apiRoot() const
128✔
1551
{
1552
  return mApiRoot;
128✔
1553
}
1554

1555
void MerginApi::setApiRoot( const QString &apiRoot )
4✔
1556
{
1557
  QString newApiRoot;
4✔
1558
  if ( apiRoot.isEmpty() )
4✔
1559
  {
1560
    newApiRoot = defaultApiRoot();
×
1561
  }
1562
  else
1563
  {
1564
    newApiRoot = apiRoot;
4✔
1565
  }
1566

1567
  if ( newApiRoot != mApiRoot )
4✔
1568
  {
1569
    mApiRoot = newApiRoot;
2✔
1570

1571
    QSettings settings;
2✔
1572
    settings.setValue( QStringLiteral( "Input/apiRoot" ), mApiRoot );
4✔
1573

1574
    emit apiRootChanged();
2✔
1575
  }
2✔
1576
}
4✔
1577

1578
QString MerginApi::merginUserName() const
28✔
1579
{
1580
  return userAuth()->username();
28✔
1581
}
1582

1583
QList<MerginFile> MerginApi::getLocalProjectFiles( const QString &projectPath )
281✔
1584
{
1585
  QList<MerginFile> merginFiles;
281✔
1586
  QSet<QString> localFiles = listFiles( projectPath );
281✔
1587
  for ( QString p : localFiles )
1,274✔
1588
  {
1589

1590
    MerginFile file;
993✔
1591
    QByteArray localChecksumBytes = getChecksum( projectPath + p );
993✔
1592
    QString localChecksum = QString::fromLatin1( localChecksumBytes.data(), localChecksumBytes.size() );
993✔
1593
    file.checksum = localChecksum;
993✔
1594
    file.path = p;
993✔
1595
    QFileInfo info( projectPath + p );
993✔
1596
    file.size = info.size();
993✔
1597
    file.mtime = info.lastModified();
993✔
1598
    merginFiles.append( file );
993✔
1599
  }
993✔
1600
  return merginFiles;
562✔
1601
}
281✔
1602

1603
void MerginApi::listProjectsReplyFinished( QString requestId )
22✔
1604
{
1605
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
22✔
1606
  Q_ASSERT( r );
22✔
1607

1608
  int projectCount = -1;
22✔
1609
  int requestedPage = 1;
22✔
1610
  MerginProjectsList projectList;
22✔
1611

1612
  if ( r->error() == QNetworkReply::NoError )
22✔
1613
  {
1614
    QUrlQuery query( r->request().url().query() );
44✔
1615
    requestedPage = query.queryItemValue( "page" ).toInt();
22✔
1616

1617
    QByteArray data = r->readAll();
22✔
1618
    QJsonDocument doc = QJsonDocument::fromJson( data );
22✔
1619

1620
    if ( doc.isObject() )
22✔
1621
    {
1622
      projectCount = doc.object().value( "count" ).toInt();
22✔
1623
      projectList = parseProjectsFromJson( doc );
22✔
1624
    }
1625

1626
    CoreUtils::log( "list projects", QStringLiteral( "Success - got %1 projects" ).arg( projectList.count() ) );
22✔
1627
  }
22✔
1628
  else
1629
  {
1630
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
1631
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listProjects" ), r->errorString(), serverMsg );
×
1632
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: listProjects" ) );
×
1633
    CoreUtils::log( "list projects", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
1634

1635
    emit listProjectsFailed();
×
1636
  }
×
1637

1638
  r->deleteLater();
22✔
1639

1640
  emit listProjectsFinished( projectList, projectCount, requestedPage, requestId );
22✔
1641
}
22✔
1642

1643
void MerginApi::listProjectsByNameReplyFinished( QString requestId )
7✔
1644
{
1645
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
7✔
1646
  Q_ASSERT( r );
7✔
1647

1648
  MerginProjectsList projectList;
7✔
1649

1650
  if ( r->error() == QNetworkReply::NoError )
7✔
1651
  {
1652
    QByteArray data = r->readAll();
7✔
1653
    QJsonDocument json = QJsonDocument::fromJson( data );
7✔
1654
    projectList = parseProjectsFromJson( json );
7✔
1655
    CoreUtils::log( "list projects by name", QStringLiteral( "Success - got %1 projects" ).arg( projectList.count() ) );
7✔
1656
  }
7✔
1657
  else
1658
  {
1659
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
1660
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listProjectsByName" ), r->errorString(), serverMsg );
×
1661
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: listProjectsByName" ) );
×
1662
    CoreUtils::log( "list projects by name", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
1663

1664
    emit listProjectsFailed();
×
1665
  }
×
1666

1667
  r->deleteLater();
7✔
1668

1669
  emit listProjectsByNameFinished( projectList, requestId );
7✔
1670
}
7✔
1671

1672

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

1677
  QString dest = projectDir + "/" + filePath;
202✔
1678
  createPathIfNotExists( dest );
202✔
1679

1680
  QFile f( dest );
202✔
1681
  if ( !f.open( QIODevice::WriteOnly ) )
202✔
1682
  {
1683
    CoreUtils::log( "pull " + projectFullName, "Failed to open file for writing " + dest );
×
1684
    return;
×
1685
  }
1686

1687
  // assemble file from tmp files
1688
  for ( const auto &item : items )
343✔
1689
  {
1690
    QFile fTmp( tempDir + "/" + item.tempFileName );
282✔
1691
    if ( !fTmp.open( QIODevice::ReadOnly ) )
141✔
1692
    {
1693
      CoreUtils::log( "pull " + projectFullName, "Failed to open temp file for reading " + item.tempFileName );
×
1694
      return;
×
1695
    }
1696
    f.write( fTmp.readAll() );
141✔
1697
  }
141✔
1698

1699
  f.close();
202✔
1700

1701
  // if diffable, copy to .mergin dir so we have a basefile
1702
  if ( MerginApi::isFileDiffable( filePath ) )
202✔
1703
  {
1704
    QString basefile = projectDir + "/.mergin/" + filePath;
15✔
1705
    createPathIfNotExists( basefile );
15✔
1706

1707
    if ( !QFile::remove( basefile ) )
15✔
1708
    {
1709
      CoreUtils::log( "pull " + projectFullName, "failed to remove old basefile for: " + filePath );
15✔
1710
    }
1711
    if ( !QFile::copy( dest, basefile ) )
15✔
1712
    {
1713
      CoreUtils::log( "pull " + projectFullName, "failed to copy new basefile for: " + filePath );
×
1714
    }
1715
  }
15✔
1716
}
202✔
1717

1718

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

1723
  // update diffable files that have been modified on the server
1724
  // - if they were not modified locally, the server changes will be simply applied
1725
  // - if they were modified locally, local changes will be rebased on top of server changes
1726

1727
  QString src = tempDir + "/" + CoreUtils::uuidWithoutBraces( QUuid::createUuid() );
18✔
1728
  QString dest = projectDir + "/" + filePath;
9✔
1729
  QString basefile = projectDir + "/.mergin/" + filePath;
9✔
1730

1731
  LocalProject info = mLocalProjects.projectFromMerginName( projectFullName );
9✔
1732

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

1736
  createPathIfNotExists( src );
9✔
1737
  createPathIfNotExists( dest );
9✔
1738
  createPathIfNotExists( basefile );
9✔
1739

1740
  QStringList diffFiles;
9✔
1741
  for ( const auto &item : items )
19✔
1742
    diffFiles << tempDir + "/" + item.tempFileName;
10✔
1743

1744
  //
1745
  // let's first assemble server's file from our basefile + diffs
1746
  //
1747

1748
  if ( !QFile::copy( basefile, src ) )
9✔
1749
  {
1750
    CoreUtils::log( "pull " + projectFullName, "assemble server file fail: copying failed " + basefile + " to " + src );
×
1751

1752
    // TODO: this is a critical failure - we should abort pull
1753
  }
1754

1755
  if ( !GeodiffUtils::applyDiffs( src, diffFiles ) )
9✔
1756
  {
1757
    CoreUtils::log( "pull " + projectFullName, "server file assembly failed: " + filePath );
×
1758

1759
    // TODO: this is a critical failure - we should abort pull
1760
    // TODO: we could try to delete the basefile and re-download it from scratch on next sync
1761
  }
1762
  else
1763
  {
1764
    CoreUtils::log( "pull " + projectFullName, "server file assembly successful: " + filePath );
9✔
1765
  }
1766

1767
  //
1768
  // now we are ready for the update of our local file
1769
  //
1770
  bool hasConflicts = false;
9✔
1771

1772
  bool res = GeodiffUtils::rebase( basefile,
9✔
1773
                                   src,
1774
                                   dest,
1775
                                   conflictfile
1776
                                 );
1777
  if ( res )
9✔
1778
  {
1779
    CoreUtils::log( "pull " + projectFullName, "geodiff rebase successful: " + filePath );
8✔
1780
  }
1781
  else
1782
  {
1783
    CoreUtils::log( "pull " + projectFullName, "geodiff rebase failed! " + filePath );
1✔
1784

1785
    // not good... something went wrong in rebase - we need to save the local changes
1786
    // let's put them into a conflict file and use the server version
1787
    hasConflicts = true;
1✔
1788
    LocalProject info = mLocalProjects.projectFromMerginName( projectFullName );
1✔
1789
    QString newDest = CoreUtils::findUniquePath( CoreUtils::generateConflictedCopyFileName( dest, mUserAuth->username(), info.localVersion ) );
2✔
1790
    if ( !QFile::rename( dest, newDest ) )
1✔
1791
    {
1792
      CoreUtils::log( "pull " + projectFullName, "failed rename of conflicting file after failed geodiff rebase: " + filePath );
×
1793
    }
1794
    if ( !QFile::copy( src, dest ) )
1✔
1795
    {
1796
      CoreUtils::log( "pull " + projectFullName, "failed to update local conflicting file after failed geodiff rebase: " + filePath );
×
1797
    }
1798
  }
1✔
1799

1800
  //
1801
  // finally update our basefile
1802
  //
1803

1804
  if ( !QFile::remove( basefile ) )
9✔
1805
  {
1806
    CoreUtils::log( "pull " + projectFullName, "failed removal of old basefile: " + filePath );
×
1807

1808
    // TODO: this is a critical failure - we should abort pull
1809
  }
1810
  if ( !QFile::rename( src, basefile ) )
9✔
1811
  {
1812
    CoreUtils::log( "pull " + projectFullName, "failed rename of basefile using new server content: " + filePath );
×
1813

1814
    // TODO: this is a critical failure - we should abort pull
1815
  }
1816
  return hasConflicts;
9✔
1817
}
9✔
1818

1819
void MerginApi::finalizeProjectPull( const QString &projectFullName )
123✔
1820
{
1821
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
123✔
1822
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
123✔
1823

1824
  QString projectDir = transaction.projectDir;
123✔
1825
  QString tempProjectDir = getTempProjectDir( projectFullName );
123✔
1826

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

1829
  for ( const PullTask &finalizationItem : transaction.pullTasks )
337✔
1830
  {
1831
    switch ( finalizationItem.method )
214✔
1832
    {
1833
      case PullTask::Copy:
199✔
1834
      {
1835
        finalizeProjectPullCopy( projectFullName, projectDir, tempProjectDir, finalizationItem.filePath, finalizationItem.data );
199✔
1836
        break;
199✔
1837
      }
1838

1839
      case PullTask::CopyConflict:
3✔
1840
      {
1841
        // move local file to conflict file
1842
        QString origPath = projectDir + "/" + finalizationItem.filePath;
3✔
1843
        LocalProject info = mLocalProjects.projectFromMerginName( projectFullName );
3✔
1844
        QString newPath = CoreUtils::findUniquePath( CoreUtils::generateConflictedCopyFileName( origPath, mUserAuth->username(), info.localVersion ) );
6✔
1845
        if ( !QFile::rename( origPath, newPath ) )
3✔
1846
        {
1847
          CoreUtils::log( "pull " + projectFullName, "failed rename of conflicting file: " + finalizationItem.filePath );
×
1848
        }
1849
        else
1850
        {
1851
          CoreUtils::log( "pull " + projectFullName, "Local file renamed due to conflict with server: " + finalizationItem.filePath );
3✔
1852
        }
1853
        finalizeProjectPullCopy( projectFullName, projectDir, tempProjectDir, finalizationItem.filePath, finalizationItem.data );
3✔
1854
        break;
3✔
1855
      }
3✔
1856

1857
      case PullTask::ApplyDiff:
9✔
1858
      {
1859
        // applying diff can result in conflicted copy too, in this case
1860
        // we need to update gpkgSchemaChanged flag.
1861
        bool res = finalizeProjectPullApplyDiff( projectFullName, projectDir, tempProjectDir, finalizationItem.filePath, finalizationItem.data );
9✔
1862
        transaction.gpkgSchemaChanged = res;
9✔
1863
        break;
9✔
1864
      }
1865

1866
      case PullTask::Delete:
3✔
1867
      {
1868
        CoreUtils::log( "pull " + projectFullName, "Removing local file: " + finalizationItem.filePath );
3✔
1869
        QFile file( projectDir + "/" + finalizationItem.filePath );
6✔
1870
        file.remove();
3✔
1871
        break;
3✔
1872
      }
3✔
1873
    }
1874

1875
    // remove tmp files associated with this item
1876
    for ( const auto &downloadItem : finalizationItem.data )
365✔
1877
    {
1878
      if ( !QFile::remove( tempProjectDir + "/" + downloadItem.tempFileName ) )
151✔
1879
        CoreUtils::log( "pull " + projectFullName, "Failed to remove temporary file " + downloadItem.tempFileName );
×
1880
    }
1881
  }
1882

1883
  // check there are no files left
1884
  int tmpFilesLeft = QDir( tempProjectDir ).entryList( QDir::NoDotAndDotDot ).count();
123✔
1885
  if ( tmpFilesLeft )
123✔
1886
  {
1887
    CoreUtils::log( "pull " + projectFullName, "Some temporary files were left - this should not happen..." );
×
1888
  }
1889

1890
  QDir( tempProjectDir ).removeRecursively();
123✔
1891

1892
  // add the local project if not there yet
1893
  if ( !mLocalProjects.projectFromMerginName( projectFullName ).isValid() )
123✔
1894
  {
1895
    QString projectNamespace, projectName;
61✔
1896
    extractProjectName( projectFullName, projectNamespace, projectName );
61✔
1897

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

1902
    mLocalProjects.addMerginProject( projectDir, projectNamespace, projectName );
61✔
1903
  }
61✔
1904

1905
  finishProjectSync( projectFullName, true );
123✔
1906
}
123✔
1907

1908

1909
void MerginApi::pushStartReplyFinished()
115✔
1910
{
1911
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
115✔
1912
  Q_ASSERT( r );
115✔
1913

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

1916
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
115✔
1917
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
115✔
1918
  Q_ASSERT( r == transaction.replyPushStart );
115✔
1919

1920
  if ( r->error() == QNetworkReply::NoError )
115✔
1921
  {
1922
    QByteArray data = r->readAll();
115✔
1923

1924
    transaction.replyPushStart->deleteLater();
115✔
1925
    transaction.replyPushStart = nullptr;
115✔
1926

1927
    QList<MerginFile> files = transaction.pushQueue;
115✔
1928
    if ( !files.isEmpty() )
115✔
1929
    {
1930
      QString transactionUUID;
111✔
1931
      QJsonDocument doc = QJsonDocument::fromJson( data );
111✔
1932
      if ( doc.isObject() )
111✔
1933
      {
1934
        QJsonObject docObj = doc.object();
111✔
1935
        transactionUUID = docObj.value( QStringLiteral( "transaction" ) ).toString();
111✔
1936
        transaction.transactionUUID = transactionUUID;
111✔
1937
      }
111✔
1938

1939
      if ( transaction.transactionUUID.isEmpty() )
111✔
1940
      {
1941
        CoreUtils::log( "push " + projectFullName, QStringLiteral( "Fail! Could not acquire transaction ID" ) );
×
1942
        finishProjectSync( projectFullName, false );
×
1943
      }
1944

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

1947
      MerginFile file = files.first();
111✔
1948
      pushFile( projectFullName, transactionUUID, file );
111✔
1949
      emit pushFilesStarted();
111✔
1950
    }
111✔
1951
    else  // pushing only files to be removed
1952
    {
1953
      // we are done here - no upload of chunks, no request to "finish"
1954
      // because server immediatelly creates a new version without starting a transaction to upload chunks
1955

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

1958
      transaction.projectMetadata = data;
4✔
1959
      transaction.version = MerginProjectMetadata::fromJson( data ).version;
4✔
1960

1961
      finishProjectSync( projectFullName, true );
4✔
1962
    }
1963
  }
115✔
1964
  else
1965
  {
1966
    QByteArray data = r->readAll();
×
1967
    QString serverMsg = extractServerErrorMsg( data );
×
1968
    QString code = extractServerErrorCode( data );
×
1969
    bool showLimitReachedDialog = EnumHelper::isEqual( code, ErrorCode::StorageLimitHit );
×
1970

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

1973
    transaction.replyPushStart->deleteLater();
×
1974
    transaction.replyPushStart = nullptr;
×
1975

1976
    if ( showLimitReachedDialog )
×
1977
    {
1978
      const QList<MerginFile> files = transaction.pushQueue;
×
1979
      qreal uploadSize = 0;
×
1980
      for ( const MerginFile &f : files )
×
1981
      {
1982
        uploadSize += f.size;
×
1983
      }
1984
      emit storageLimitReached( uploadSize );
×
1985

1986
      // remove project if it was first time sync - migration
1987
      if ( transaction.isInitialPush )
×
1988
      {
1989
        QString projectNamespace, projectName;
×
1990
        extractProjectName( projectFullName, projectNamespace, projectName );
×
1991

1992
        detachProjectFromMergin( projectNamespace, projectName, false );
×
1993
        deleteProject( projectNamespace, projectName, false );
×
1994
      }
×
1995
    }
×
1996
    else
1997
    {
1998
      int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
×
1999
      emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: pushStartReply" ), httpCode, projectFullName );
×
2000
    }
2001
    finishProjectSync( projectFullName, false );
×
2002
  }
×
2003
}
115✔
2004

2005
void MerginApi::pushFileReplyFinished()
152✔
2006
{
2007
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
152✔
2008
  Q_ASSERT( r );
152✔
2009

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

2012
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
152✔
2013
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
152✔
2014
  Q_ASSERT( r == transaction.replyPushFile );
152✔
2015

2016
  QStringList params = ( r->url().toString().split( "/" ) );
304✔
2017
  QString transactionUUID = params.at( params.length() - 2 );
152✔
2018
  QString chunkID = params.at( params.length() - 1 );
152✔
2019
  Q_ASSERT( transactionUUID == transaction.transactionUUID );
152✔
2020

2021
  if ( r->error() == QNetworkReply::NoError )
152✔
2022
  {
2023
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Uploaded successfully: " ) + chunkID );
151✔
2024

2025
    transaction.replyPushFile->deleteLater();
151✔
2026
    transaction.replyPushFile = nullptr;
151✔
2027

2028
    MerginFile currentFile = transaction.pushQueue.first();
151✔
2029
    int chunkNo = currentFile.chunks.indexOf( chunkID );
151✔
2030
    if ( chunkNo < currentFile.chunks.size() - 1 )
151✔
2031
    {
2032
      pushFile( projectFullName, transactionUUID, currentFile, chunkNo + 1 );
2✔
2033
    }
2034
    else
2035
    {
2036
      transaction.transferedSize += currentFile.size;
149✔
2037

2038
      emit syncProjectStatusChanged( projectFullName, transaction.transferedSize / transaction.totalSize );
149✔
2039
      transaction.pushQueue.removeFirst();
149✔
2040

2041
      if ( !transaction.pushQueue.isEmpty() )
149✔
2042
      {
2043
        MerginFile nextFile = transaction.pushQueue.first();
39✔
2044
        pushFile( projectFullName, transactionUUID, nextFile );
39✔
2045
      }
39✔
2046
      else
2047
      {
2048
        pushFinish( projectFullName, transactionUUID );
110✔
2049
      }
2050
    }
2051
  }
151✔
2052
  else
2053
  {
2054
    QString serverMsg = extractServerErrorMsg( r->readAll() );
2✔
2055
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) );
1✔
2056

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

2060
    transaction.replyPushFile->deleteLater();
1✔
2061
    transaction.replyPushFile = nullptr;
1✔
2062

2063
    finishProjectSync( projectFullName, false );
1✔
2064
  }
1✔
2065
}
152✔
2066

2067
void MerginApi::pullInfoReplyFinished()
100✔
2068
{
2069
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
100✔
2070
  Q_ASSERT( r );
100✔
2071

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

2074
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
100✔
2075
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
100✔
2076
  Q_ASSERT( r == transaction.replyPullProjectInfo );
100✔
2077

2078
  if ( r->error() == QNetworkReply::NoError )
100✔
2079
  {
2080
    QByteArray data = r->readAll();
99✔
2081
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Downloaded project info." ) );
99✔
2082

2083
    transaction.replyPullProjectInfo->deleteLater();
99✔
2084
    transaction.replyPullProjectInfo = nullptr;
99✔
2085

2086
    prepareProjectPull( projectFullName, data );
99✔
2087
  }
99✔
2088
  else
2089
  {
2090
    QString serverMsg = extractServerErrorMsg( r->readAll() );
2✔
2091
    QString message = QStringLiteral( "Network API error: %1(): %2" ).arg( QStringLiteral( "projectInfo" ), r->errorString() );
3✔
2092
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) );
1✔
2093

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

2097
    transaction.replyPullProjectInfo->deleteLater();
1✔
2098
    transaction.replyPullProjectInfo = nullptr;
1✔
2099

2100
    finishProjectSync( projectFullName, false );
1✔
2101
  }
1✔
2102
}
100✔
2103

2104
void MerginApi::prepareProjectPull( const QString &projectFullName, const QByteArray &data )
125✔
2105
{
2106
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
125✔
2107
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
125✔
2108

2109
  MerginProjectMetadata serverProject = MerginProjectMetadata::fromJson( data );
125✔
2110

2111
  transaction.projectMetadata = data;
125✔
2112
  transaction.version = serverProject.version;
125✔
2113

2114
  LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName );
125✔
2115
  if ( projectInfo.isValid() )
125✔
2116
  {
2117
    transaction.projectDir = projectInfo.projectDir;
63✔
2118

2119
    // do not continue if we are already on the latest version
2120
    if ( projectInfo.localVersion != -1 && projectInfo.localVersion == serverProject.version )
63✔
2121
    {
2122
      emit projectAlreadyOnLatestVersion( projectFullName );
1✔
2123
      CoreUtils::log( QStringLiteral( "Pull %1" ).arg( projectFullName ), QStringLiteral( "Project is already on the latest version: %1" ).arg( serverProject.version ) );
2✔
2124

2125
      return finishProjectSync( projectFullName, false );
1✔
2126
    }
2127
  }
2128
  else
2129
  {
2130
    QString projectNamespace;
62✔
2131
    QString projectName;
62✔
2132
    extractProjectName( projectFullName, projectNamespace, projectName );
62✔
2133

2134
    // remove any leftover temp files that could be created from previous unsuccessful download
2135
    removeProjectsTempFolder( projectNamespace, projectName );
62✔
2136

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

2141
    // create file indicating first time download in progress
2142
    QString downloadInProgressFilePath = CoreUtils::downloadInProgressFilePath( transaction.projectDir );
62✔
2143
    createPathIfNotExists( downloadInProgressFilePath );
62✔
2144
    if ( !CoreUtils::createEmptyFile( downloadInProgressFilePath ) )
62✔
2145
      CoreUtils::log( QStringLiteral( "pull %1" ).arg( projectFullName ), "Unable to create temporary download in progress file" );
×
2146

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

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

2152
  if ( transaction.configAllowed )
124✔
2153
  {
2154
    prepareDownloadConfig( projectFullName );
112✔
2155
  }
2156
  else
2157
  {
2158
    startProjectPull( projectFullName );
12✔
2159
  }
2160
}
126✔
2161

2162
void MerginApi::startProjectPull( const QString &projectFullName )
124✔
2163
{
2164
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
124✔
2165
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
124✔
2166

2167
  QList<MerginFile> localFiles = getLocalProjectFiles( transaction.projectDir + "/" );
124✔
2168
  MerginProjectMetadata serverProject = MerginProjectMetadata::fromJson( transaction.projectMetadata );
124✔
2169
  MerginProjectMetadata oldServerProject = MerginProjectMetadata::fromCachedJson( transaction.projectDir + "/" + sMetadataFile );
248✔
2170
  MerginConfig oldTransactionConfig = MerginConfig::fromFile( transaction.projectDir + "/" + sMerginConfigFile );
248✔
2171

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

2175
  transaction.diff = compareProjectFiles(
124✔
2176
                       oldServerProject.files,
2177
                       serverProject.files,
2178
                       localFiles,
2179
                       transaction.projectDir,
124✔
2180
                       transaction.configAllowed,
124✔
2181
                       transaction.config,
124✔
2182
                       oldTransactionConfig );
124✔
2183

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

2186
  for ( QString filePath : transaction.diff.remoteAdded )
317✔
2187
  {
2188
    MerginFile file = serverProject.fileInfo( filePath );
193✔
2189
    QList<DownloadQueueItem> items = itemsForFileChunks( file, transaction.version );
193✔
2190
    transaction.pullTasks << PullTask( PullTask::Copy, filePath, items );
193✔
2191
    transaction.gpkgSchemaChanged = true;
193✔
2192
  }
193✔
2193

2194
  for ( QString filePath : transaction.diff.remoteUpdated )
138✔
2195
  {
2196
    MerginFile file = serverProject.fileInfo( filePath );
14✔
2197

2198
    // for diffable files - download and apply to the basefile (without rebase)
2199
    if ( isFileDiffable( filePath ) && file.pullCanUseDiff )
14✔
2200
    {
2201
      QList<DownloadQueueItem> items = itemsForFileDiffs( file );
6✔
2202
      transaction.pullTasks << PullTask( PullTask::ApplyDiff, filePath, items );
6✔
2203
    }
6✔
2204
    else
2205
    {
2206
      QList<DownloadQueueItem> items = itemsForFileChunks( file, transaction.version );
8✔
2207
      transaction.pullTasks << PullTask( PullTask::Copy, filePath, items );
8✔
2208
      transaction.gpkgSchemaChanged = true;
8✔
2209
    }
8✔
2210
  }
14✔
2211

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

2217
    // for diffable files - download and apply to the basefile (will also do rebase)
2218
    if ( isFileDiffable( filePath ) && file.pullCanUseDiff )
5✔
2219
    {
2220
      QList<DownloadQueueItem> items = itemsForFileDiffs( file );
3✔
2221
      transaction.pullTasks << PullTask( PullTask::ApplyDiff, filePath, items );
3✔
2222
    }
3✔
2223
    else
2224
    {
2225
      QList<DownloadQueueItem> items = itemsForFileChunks( file, transaction.version );
2✔
2226
      transaction.pullTasks << PullTask( PullTask::CopyConflict, filePath, items );
2✔
2227
      transaction.gpkgSchemaChanged = true;
2✔
2228
    }
2✔
2229
  }
5✔
2230

2231
  // also download files which were added both on the server and locally (the local version will be renamed as conflicting copy)
2232
  for ( QString filePath : transaction.diff.conflictRemoteAddedLocalAdded )
125✔
2233
  {
2234
    MerginFile file = serverProject.fileInfo( filePath );
1✔
2235
    QList<DownloadQueueItem> items = itemsForFileChunks( file, transaction.version );
1✔
2236
    transaction.pullTasks << PullTask( PullTask::CopyConflict, filePath, items );
1✔
2237
    transaction.gpkgSchemaChanged = true;
1✔
2238
  }
1✔
2239

2240
  // schedule removed files to be deleted
2241
  for ( QString filePath : transaction.diff.remoteDeleted )
127✔
2242
  {
2243
    transaction.pullTasks << PullTask( PullTask::Delete, filePath, QList<DownloadQueueItem>() );
3✔
2244
  }
3✔
2245

2246
  // prepare the download queue
2247
  for ( const PullTask &item : transaction.pullTasks )
340✔
2248
  {
2249
    transaction.downloadQueue << item.data;
216✔
2250
  }
2251

2252
  qint64 totalSize = 0;
124✔
2253
  for ( const DownloadQueueItem &item : transaction.downloadQueue )
277✔
2254
  {
2255
    totalSize += item.size;
153✔
2256
  }
2257
  transaction.totalSize = totalSize;
124✔
2258

2259
  CoreUtils::log( "pull " + projectFullName, QStringLiteral( "%1 update tasks, %2 items to download (total size %3 bytes)" )
372✔
2260
                  .arg( transaction.pullTasks.count() )
248✔
2261
                  .arg( transaction.downloadQueue.count() )
248✔
2262
                  .arg( transaction.totalSize ) );
248✔
2263

2264
  emit pullFilesStarted();
124✔
2265
  downloadNextItem( projectFullName );
124✔
2266
}
124✔
2267

2268
void MerginApi::prepareDownloadConfig( const QString &projectFullName, bool downloaded )
151✔
2269
{
2270
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
151✔
2271
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
151✔
2272

2273
  MerginProjectMetadata newServerVersion = MerginProjectMetadata::fromJson( transaction.projectMetadata );
151✔
2274

2275
  const auto res = std::find_if( newServerVersion.files.begin(), newServerVersion.files.end(), []( const MerginFile & file )
151✔
2276
  {
2277
    return file.path == sMerginConfigFile;
531✔
2278
  } );
2279
  bool serverContainsConfig = res != newServerVersion.files.end();
151✔
2280

2281
  if ( serverContainsConfig )
151✔
2282
  {
2283
    if ( !downloaded )
78✔
2284
    {
2285
      // we should have server config but we do not have it yet
2286
      return requestServerConfig( projectFullName );
39✔
2287
    }
2288
  }
2289

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

2292
  const auto resOld = std::find_if( oldServerVersion.files.begin(), oldServerVersion.files.end(), []( const MerginFile & file )
112✔
2293
  {
2294
    return file.path == sMerginConfigFile;
205✔
2295
  } );
2296

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

2299
  if ( !transaction.config.isValid )
112✔
2300
  {
2301
    // if transaction is not valid (or missing), consider it as deleted
2302
    transaction.config.downloadMissingFiles = true;
74✔
2303
    CoreUtils::log( "MerginConfig", "No config detected" );
74✔
2304
  }
2305
  else if ( serverContainsConfig && previousVersionContainedConfig )
38✔
2306
  {
2307
    // config was there, check if there are changes
2308
    QString newChk = newServerVersion.fileInfo( sMerginConfigFile ).checksum;
29✔
2309
    QString oldChk = oldServerVersion.fileInfo( sMerginConfigFile ).checksum;
29✔
2310

2311
    if ( newChk == oldChk )
29✔
2312
    {
2313
      // config files are the same
2314
    }
2315
    else
2316
    {
2317
      // config was changed, but what changed?
2318
      MerginConfig oldConfig = MerginConfig::fromFile( transaction.projectDir + "/" + MerginApi::sMerginConfigFile );
16✔
2319

2320
      if ( oldConfig.selectiveSyncEnabled != transaction.config.selectiveSyncEnabled )
8✔
2321
      {
2322
        // selective sync was enabled/disabled
2323
        if ( transaction.config.selectiveSyncEnabled )
4✔
2324
        {
2325
          CoreUtils::log( "MerginConfig", "Selective sync has been enabled" );
2✔
2326
        }
2327
        else
2328
        {
2329
          CoreUtils::log( "MerginConfig", "Selective sync has been disabled, downloading missing files." );
2✔
2330
          transaction.config.downloadMissingFiles = true;
2✔
2331
        }
2332
      }
2333
      else if ( oldConfig.selectiveSyncDir != transaction.config.selectiveSyncDir )
4✔
2334
      {
2335
        CoreUtils::log( "MerginConfig", "Selective sync directory has changed, downloading missing files." );
4✔
2336
        transaction.config.downloadMissingFiles = true;
4✔
2337
      }
2338
      else
2339
      {
2340
        CoreUtils::log( "MerginConfig", "Unknown change in config file, continuing with latest version." );
×
2341
      }
2342
    }
8✔
2343
  }
29✔
2344
  else if ( serverContainsConfig )
9✔
2345
  {
2346
    CoreUtils::log( "MerginConfig", "Detected new config file." );
9✔
2347
  }
2348
  else if ( previousVersionContainedConfig ) // and current does not
×
2349
  {
2350
    CoreUtils::log( "MerginConfig", "Config file was removed, downloading missing files." );
×
2351
    transaction.config.downloadMissingFiles = true;
×
2352
  }
2353
  else // no config in last versions
2354
  {
2355
    // pull like without config
2356
    transaction.configAllowed = false;
×
2357
    transaction.config.isValid = false;
×
2358

2359
    // if it would be possible to add mergin-config locally, it needs to be checked here
2360
  }
2361

2362
  startProjectPull( projectFullName );
112✔
2363
}
151✔
2364

2365
void MerginApi::requestServerConfig( const QString &projectFullName )
39✔
2366
{
2367
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
39✔
2368
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
39✔
2369

2370
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/raw/" ) + projectFullName );
78✔
2371
  QUrlQuery query;
39✔
2372

2373
  query.addQueryItem( "file", sMerginConfigFile.toUtf8().toPercentEncoding() );
39✔
2374
  query.addQueryItem( "version", QStringLiteral( "v%1" ).arg( transaction.version ) );
39✔
2375
  url.setQuery( query );
39✔
2376

2377
  QNetworkRequest request = getDefaultRequest();
39✔
2378
  request.setUrl( url );
39✔
2379
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
39✔
2380

2381
  Q_ASSERT( !transaction.replyPullItem );
39✔
2382
  transaction.replyPullItem = mManager.get( request );
39✔
2383
  connect( transaction.replyPullItem, &QNetworkReply::finished, this, &MerginApi::cacheServerConfig );
39✔
2384

2385
  CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Requesting mergin config: " ) + url.toString() );
78✔
2386
}
39✔
2387

2388
QList<DownloadQueueItem> MerginApi::itemsForFileChunks( const MerginFile &file, int version )
204✔
2389
{
2390
  QList<DownloadQueueItem> lst;
204✔
2391
  int from = 0;
204✔
2392
  while ( from < file.size )
347✔
2393
  {
2394
    int size = qMin( MerginApi::UPLOAD_CHUNK_SIZE, static_cast<int>( file.size ) - from );
143✔
2395
    lst << DownloadQueueItem( file.path, size, version, from, from + size - 1 );
143✔
2396
    from += size;
143✔
2397
  }
2398
  return lst;
204✔
2399
}
×
2400

2401
QList<DownloadQueueItem> MerginApi::itemsForFileDiffs( const MerginFile &file )
9✔
2402
{
2403
  QList<DownloadQueueItem> items;
9✔
2404
  // download diffs instead of full download of gpkg file from server
2405
  for ( const auto &d : file.pullDiffFiles )
19✔
2406
  {
2407
    items << DownloadQueueItem( file.path, d.second, d.first, -1, -1, true );
10✔
2408
  }
2409
  return items;
9✔
2410
}
×
2411

2412

2413
static MerginFile findFile( const QString &filePath, const QList<MerginFile> &files )
155✔
2414
{
2415
  for ( const MerginFile &merginFile : files )
359✔
2416
  {
2417
    if ( merginFile.path == filePath )
359✔
2418
      return merginFile;
155✔
2419
  }
2420
  CoreUtils::log( QStringLiteral( "MerginFile" ), QStringLiteral( "requested findFile() for non-existant file: %1" ).arg( filePath ) );
×
2421
  return MerginFile();
×
2422
}
2423

2424

2425
void MerginApi::pushInfoReplyFinished()
143✔
2426
{
2427
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
143✔
2428
  Q_ASSERT( r );
143✔
2429

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

2432
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
143✔
2433
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
143✔
2434
  Q_ASSERT( r == transaction.replyPushProjectInfo );
143✔
2435

2436
  if ( r->error() == QNetworkReply::NoError )
143✔
2437
  {
2438
    QString url = r->url().toString();
142✔
2439
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Downloaded project info." ) );
142✔
2440
    QByteArray data = r->readAll();
142✔
2441

2442
    transaction.replyPushProjectInfo->deleteLater();
142✔
2443
    transaction.replyPushProjectInfo = nullptr;
142✔
2444

2445
    LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName );
142✔
2446
    transaction.projectDir = projectInfo.projectDir;
142✔
2447
    Q_ASSERT( !transaction.projectDir.isEmpty() );
142✔
2448

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

2452
    // now let's figure a key question: are we on the most recent version of the project
2453
    // if we're about to do upload? because if not, we need to do pull first
2454
    if ( projectInfo.isValid() && projectInfo.localVersion != -1 && projectInfo.localVersion < serverProject.version )
142✔
2455
    {
2456
      CoreUtils::log( "push " + projectFullName, QStringLiteral( "Need pull first: local version %1 | server version %2" )
78✔
2457
                      .arg( projectInfo.localVersion ).arg( serverProject.version ) );
52✔
2458
      transaction.pullBeforePush = true;
26✔
2459
      prepareProjectPull( projectFullName, data );
26✔
2460
      return;
26✔
2461
    }
2462

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

2466
    // Cache mergin-config, since we are on the most recent version, it is sufficient to just read the local version
2467
    if ( transaction.configAllowed )
116✔
2468
    {
2469
      transaction.config = MerginConfig::fromFile( transaction.projectDir + "/" + MerginApi::sMerginConfigFile );
107✔
2470
    }
2471

2472
    transaction.diff = compareProjectFiles(
232✔
2473
                         oldServerProject.files,
2474
                         serverProject.files,
2475
                         localFiles,
2476
                         transaction.projectDir,
116✔
2477
                         transaction.configAllowed,
116✔
2478
                         transaction.config
116✔
2479
                       );
116✔
2480

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

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

2485
    QList<MerginFile> filesToUpload;
116✔
2486
    QList<MerginFile> addedMerginFiles, updatedMerginFiles, deletedMerginFiles;
116✔
2487
    QList<MerginFile> diffFiles;
116✔
2488
    for ( QString filePath : transaction.diff.localAdded )
248✔
2489
    {
2490
      MerginFile merginFile = findFile( filePath, localFiles );
132✔
2491
      merginFile.chunks = generateChunkIdsForSize( merginFile.size );
132✔
2492
      addedMerginFiles.append( merginFile );
132✔
2493
    }
132✔
2494

2495
    for ( QString filePath : transaction.diff.localUpdated )
135✔
2496
    {
2497
      MerginFile merginFile = findFile( filePath, localFiles );
19✔
2498
      merginFile.chunks = generateChunkIdsForSize( merginFile.size );
19✔
2499

2500
      if ( MerginApi::isFileDiffable( filePath ) )
19✔
2501
      {
2502
        // try to create a diff
2503
        QString diffName;
12✔
2504
        int geodiffRes = GeodiffUtils::createChangeset( transaction.projectDir, filePath, diffName );
12✔
2505
        QString diffPath = transaction.projectDir + "/.mergin/" + diffName;
12✔
2506
        QString basePath = transaction.projectDir + "/.mergin/" + filePath;
12✔
2507

2508
        if ( geodiffRes == GEODIFF_SUCCESS )
12✔
2509
        {
2510
          QByteArray checksumDiff = getChecksum( diffPath );
12✔
2511

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

2516
          merginFile.diffName = diffName;
12✔
2517
          merginFile.diffChecksum = QString::fromLatin1( checksumDiff.data(), checksumDiff.size() );
12✔
2518
          merginFile.diffSize = QFileInfo( diffPath ).size();
12✔
2519
          merginFile.chunks = generateChunkIdsForSize( merginFile.diffSize );
12✔
2520
          merginFile.diffBaseChecksum = QString::fromLatin1( checksumBase.data(), checksumBase.size() );
12✔
2521

2522
          diffFiles.append( merginFile );
12✔
2523

2524
          CoreUtils::log( "push " + projectFullName, QString( "Geodiff create changeset on %1 successful: total size %2 bytes" ).arg( filePath ).arg( merginFile.diffSize ) );
12✔
2525
        }
12✔
2526
        else
2527
        {
2528
          // TODO: remove the diff file (if exists)
2529
          CoreUtils::log( "push " + projectFullName, QString( "Geodiff create changeset on %1 FAILED with error %2 (will do full upload)" ).arg( filePath ).arg( geodiffRes ) );
×
2530
        }
2531
      }
12✔
2532

2533
      updatedMerginFiles.append( merginFile );
19✔
2534
    }
19✔
2535

2536
    for ( QString filePath : transaction.diff.localDeleted )
120✔
2537
    {
2538
      MerginFile merginFile = findFile( filePath, serverProject.files );
4✔
2539
      deletedMerginFiles.append( merginFile );
4✔
2540
    }
4✔
2541

2542
    if ( addedMerginFiles.isEmpty() && updatedMerginFiles.isEmpty() && deletedMerginFiles.isEmpty() )
116✔
2543
    {
2544
      // if nothing has changed, there is no point to even start upload transaction
2545
      transaction.projectMetadata = data;
1✔
2546
      transaction.version = MerginProjectMetadata::fromJson( data ).version;
1✔
2547

2548
      finishProjectSync( projectFullName, true );
1✔
2549
      return;
1✔
2550
    }
2551

2552
    QJsonArray added = prepareUploadChangesJSON( addedMerginFiles );
115✔
2553
    filesToUpload.append( addedMerginFiles );
115✔
2554

2555
    QJsonArray modified = prepareUploadChangesJSON( updatedMerginFiles );
115✔
2556
    filesToUpload.append( updatedMerginFiles );
115✔
2557

2558
    QJsonArray removed = prepareUploadChangesJSON( deletedMerginFiles );
115✔
2559
    // removed not in filesToUpload
2560

2561
    QJsonObject changes;
115✔
2562
    changes.insert( "added", added );
115✔
2563
    changes.insert( "removed", removed );
115✔
2564
    changes.insert( "updated", modified );
115✔
2565
    changes.insert( "renamed", QJsonArray() );
115✔
2566

2567
    qint64 totalSize = 0;
115✔
2568
    for ( MerginFile file : filesToUpload )
266✔
2569
    {
2570
      if ( !file.diffName.isEmpty() )
151✔
2571
        totalSize += file.diffSize;
12✔
2572
      else
2573
        totalSize += file.size;
139✔
2574
    }
151✔
2575

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

2579
    transaction.totalSize = totalSize;
115✔
2580
    transaction.pushQueue = filesToUpload;
115✔
2581
    transaction.pushDiffFiles = diffFiles;
115✔
2582

2583
    QJsonObject json;
115✔
2584
    json.insert( QStringLiteral( "changes" ), changes );
230✔
2585
    json.insert( QStringLiteral( "version" ), QString( "v%1" ).arg( serverProject.version ) );
230✔
2586
    QJsonDocument jsonDoc;
115✔
2587
    jsonDoc.setObject( json );
115✔
2588

2589
    pushStart( projectFullName, jsonDoc.toJson( QJsonDocument::Compact ) );
115✔
2590
  }
230✔
2591
  else
2592
  {
2593
    QString serverMsg = extractServerErrorMsg( r->readAll() );
2✔
2594
    QString message = QStringLiteral( "Network API error: %1(): %2" ).arg( QStringLiteral( "projectInfo" ), r->errorString() );
3✔
2595
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) );
1✔
2596

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

2600
    transaction.replyPushProjectInfo->deleteLater();
1✔
2601
    transaction.replyPushProjectInfo = nullptr;
1✔
2602

2603
    finishProjectSync( projectFullName, false );
1✔
2604
  }
1✔
2605
}
143✔
2606

2607
void MerginApi::pushFinishReplyFinished()
110✔
2608
{
2609
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
110✔
2610
  Q_ASSERT( r );
110✔
2611

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

2614
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
110✔
2615
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
110✔
2616
  Q_ASSERT( r == transaction.replyPushFinish );
110✔
2617

2618
  if ( r->error() == QNetworkReply::NoError )
110✔
2619
  {
2620
    Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
110✔
2621
    QByteArray data = r->readAll();
110✔
2622
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Transaction finish accepted" ) );
110✔
2623

2624
    transaction.replyPushFinish->deleteLater();
110✔
2625
    transaction.replyPushFinish = nullptr;
110✔
2626

2627
    transaction.projectMetadata = data;
110✔
2628
    transaction.version = MerginProjectMetadata::fromJson( data ).version;
110✔
2629

2630
    //  a new diffable files suppose to have their basefile copies in .mergin
2631
    for ( QString filePath : transaction.diff.localAdded )
240✔
2632
    {
2633
      if ( MerginApi::isFileDiffable( filePath ) )
130✔
2634
      {
2635
        QString basefile = transaction.projectDir + "/.mergin/" + filePath;
11✔
2636
        createPathIfNotExists( basefile );
11✔
2637

2638
        QString sourcePath = transaction.projectDir + "/" + filePath;
11✔
2639
        if ( !QFile::copy( sourcePath, basefile ) )
11✔
2640
        {
2641
          CoreUtils::log( "push " + projectFullName, "failed to copy new basefile for: " + filePath );
×
2642
        }
2643
      }
11✔
2644
    }
130✔
2645

2646
    // clean up diff-related files
2647
    const auto diffFiles = transaction.pushDiffFiles;
110✔
2648
    for ( const MerginFile &merginFile : diffFiles )
122✔
2649
    {
2650
      QString diffPath = transaction.projectDir + "/.mergin/" + merginFile.diffName;
12✔
2651

2652
      // update basefile (unmodified file that should be equivalent to the server)
2653
      QString basePath = transaction.projectDir + "/.mergin/" + merginFile.path;
12✔
2654
      bool res = GeodiffUtils::applyChangeset( basePath, diffPath );
12✔
2655
      if ( res )
12✔
2656
      {
2657
        CoreUtils::log( "push " + projectFullName, QString( "Applied %1 to base file of %2" ).arg( merginFile.diffName, merginFile.path ) );
12✔
2658
      }
2659
      else
2660
      {
2661
        CoreUtils::log( "push " + projectFullName, QString( "Failed to apply changeset %1 to basefile %2 - error %3" ).arg( diffPath ).arg( basePath ).arg( res ) );
×
2662
      }
2663

2664
      // remove temporary diff files
2665
      if ( !QFile::remove( diffPath ) )
12✔
2666
        CoreUtils::log( "push " + projectFullName, "Failed to remove diff: " + diffPath );
×
2667
    }
12✔
2668

2669
    finishProjectSync( projectFullName, true );
110✔
2670
  }
110✔
2671
  else
2672
  {
2673
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
2674
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "pushFinish" ), r->errorString(), serverMsg );
×
2675
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) );
×
2676

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

2680
    // remove temporary diff files
2681
    const auto diffFiles = transaction.pushDiffFiles;
×
2682
    for ( const MerginFile &merginFile : diffFiles )
×
2683
    {
2684
      QString diffPath = transaction.projectDir + "/.mergin/" + merginFile.diffName;
×
2685
      if ( !QFile::remove( diffPath ) )
×
2686
        CoreUtils::log( "push " + projectFullName, "Failed to remove diff: " + diffPath );
×
2687
    }
×
2688

2689
    transaction.replyPushFinish->deleteLater();
×
2690
    transaction.replyPushFinish = nullptr;
×
2691

2692
    finishProjectSync( projectFullName, false );
×
2693
  }
×
2694
}
110✔
2695

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

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

2703
  if ( r->error() == QNetworkReply::NoError )
1✔
2704
  {
2705
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Transaction canceled" ) );
1✔
2706
  }
2707
  else
2708
  {
2709
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
2710
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "uploadCancel" ), r->errorString(), serverMsg );
×
2711
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) );
×
2712
  }
×
2713

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

2716
  r->deleteLater();
1✔
2717
}
1✔
2718

2719
void MerginApi::getUserInfoFinished()
19✔
2720
{
2721
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
19✔
2722
  Q_ASSERT( r );
19✔
2723

2724
  if ( r->error() == QNetworkReply::NoError )
19✔
2725
  {
2726
    CoreUtils::log( "user info", QStringLiteral( "Success" ) );
18✔
2727
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
18✔
2728
    if ( doc.isObject() )
18✔
2729
    {
2730
      QJsonObject docObj = doc.object();
18✔
2731
      mUserInfo->setFromJson( docObj );
18✔
2732
      if ( mServerType == MerginServerType::OLD )
18✔
2733
      {
2734
        mWorkspaceInfo->setFromJson( docObj );
×
2735
      }
2736
    }
18✔
2737
  }
18✔
2738
  else
2739
  {
2740
    QString serverMsg = extractServerErrorMsg( r->readAll() );
2✔
2741
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "getUserInfo" ), r->errorString(), serverMsg );
3✔
2742
    CoreUtils::log( "user info", QStringLiteral( "FAILED - %1" ).arg( message ) );
1✔
2743
    mUserInfo->clear();
1✔
2744
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: getUserInfo" ) );
2✔
2745
  }
1✔
2746

2747
  emit userInfoReplyFinished();
19✔
2748

2749
  r->deleteLater();
19✔
2750
}
19✔
2751

2752
void MerginApi::getWorkspaceInfoReplyFinished()
25✔
2753
{
2754
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
25✔
2755
  Q_ASSERT( r );
25✔
2756

2757
  if ( r->error() == QNetworkReply::NoError )
25✔
2758
  {
2759
    CoreUtils::log( "workspace info", QStringLiteral( "Success" ) );
25✔
2760
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
25✔
2761
    if ( doc.isObject() )
25✔
2762
    {
2763
      QJsonObject docObj = doc.object();
25✔
2764
      mWorkspaceInfo->setFromJson( docObj );
25✔
2765

2766
      emit getWorkspaceInfoFinished();
25✔
2767
    }
25✔
2768
  }
25✔
2769
  else
2770
  {
2771
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
2772
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "getWorkspaceInfo" ), r->errorString(), serverMsg );
×
2773
    CoreUtils::log( "workspace info", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
2774
    mWorkspaceInfo->clear();
×
2775
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: getWorkspaceInfo" ) );
×
2776
  }
×
2777

2778
  r->deleteLater();
25✔
2779
}
25✔
2780

2781
ProjectDiff MerginApi::compareProjectFiles(
281✔
2782
  const QList<MerginFile> &oldServerFiles,
2783
  const QList<MerginFile> &newServerFiles,
2784
  const QList<MerginFile> &localFiles,
2785
  const QString &projectDir,
2786
  bool allowConfig,
2787
  const MerginConfig &config,
2788
  const MerginConfig &lastSyncConfig
2789
)
2790
{
2791
  ProjectDiff diff;
281✔
2792
  QHash<QString, MerginFile> oldServerFilesMap, newServerFilesMap;
281✔
2793

2794
  for ( MerginFile file : newServerFiles )
1,481✔
2795
  {
2796
    newServerFilesMap.insert( file.path, file );
1,200✔
2797
  }
1,200✔
2798
  for ( MerginFile file : oldServerFiles )
1,275✔
2799
  {
2800
    oldServerFilesMap.insert( file.path, file );
994✔
2801
  }
994✔
2802

2803
  for ( MerginFile localFile : localFiles )
1,274✔
2804
  {
2805
    QString filePath = localFile.path;
993✔
2806
    bool hasOldServer = oldServerFilesMap.contains( localFile.path );
993✔
2807
    bool hasNewServer = newServerFilesMap.contains( localFile.path );
993✔
2808
    QString chkOld = oldServerFilesMap.value( localFile.path ).checksum;
993✔
2809
    QString chkNew = newServerFilesMap.value( localFile.path ).checksum;
993✔
2810
    QString chkLocal = localFile.checksum;
993✔
2811

2812
    if ( !hasOldServer && !hasNewServer )
993✔
2813
    {
2814
      // L-A
2815
      diff.localAdded << filePath;
177✔
2816
    }
2817
    else if ( hasOldServer && !hasNewServer )
816✔
2818
    {
2819
      if ( chkOld == chkLocal )
4✔
2820
      {
2821
        // R-D
2822
        diff.remoteDeleted << filePath;
3✔
2823
      }
2824
      else
2825
      {
2826
        // C/R-D/L-U
2827
        diff.conflictRemoteDeletedLocalUpdated << filePath;
1✔
2828
      }
2829
    }
2830
    else if ( !hasOldServer && hasNewServer )
812✔
2831
    {
2832
      if ( chkNew != chkLocal )
1✔
2833
      {
2834
        // C/R-A/L-A
2835
        diff.conflictRemoteAddedLocalAdded << filePath;
1✔
2836
      }
2837
      else
2838
      {
2839
        // R-A/L-A
2840
        // TODO: need to do anything?
2841
      }
2842
    }
2843
    else if ( hasOldServer && hasNewServer )
811✔
2844
    {
2845
      // file has already existed
2846
      if ( chkOld == chkNew )
811✔
2847
      {
2848
        if ( chkNew != chkLocal )
792✔
2849
        {
2850
          // L-U
2851
          if ( isFileDiffable( filePath ) )
37✔
2852
          {
2853
            // we need to do a diff here to figure out whether the file is actually changed or not
2854
            // because the real content may be the same although the checksums do not match
2855
            if ( GeodiffUtils::hasPendingChanges( projectDir, filePath ) )
29✔
2856
              diff.localUpdated << filePath;
17✔
2857
          }
2858
          else
2859
            diff.localUpdated << filePath;
8✔
2860
        }
2861
        else
2862
        {
2863
          // no change :-)
2864
        }
2865
      }
2866
      else   // v1 != v2
2867
      {
2868
        if ( chkNew != chkLocal && chkOld != chkLocal )
19✔
2869
        {
2870
          // C/R-U/L-U
2871
          if ( isFileDiffable( filePath ) )
7✔
2872
          {
2873
            // we need to do a diff here to figure out whether the file is actually changed or not
2874
            // because the real content may be the same although the checksums do not match
2875
            if ( GeodiffUtils::hasPendingChanges( projectDir, filePath ) )
5✔
2876
              diff.conflictRemoteUpdatedLocalUpdated << filePath;
3✔
2877
            else
2878
              diff.remoteUpdated << filePath;
2✔
2879
          }
2880
          else
2881
            diff.conflictRemoteUpdatedLocalUpdated << filePath;
2✔
2882
        }
2883
        else if ( chkNew != chkLocal )  // && old == local
12✔
2884
        {
2885
          // R-U
2886
          diff.remoteUpdated << filePath;
12✔
2887
        }
2888
        else if ( chkOld != chkLocal )  // && new == local
×
2889
        {
2890
          // R-U/L-U
2891
          // TODO: need to do anything?
2892
        }
2893
        else
2894
          Q_ASSERT( false );   // impossible - should be handled already
×
2895
      }
2896
    }
2897

2898
    if ( hasOldServer )
993✔
2899
      oldServerFilesMap.remove( filePath );
815✔
2900
    if ( hasNewServer )
993✔
2901
      newServerFilesMap.remove( filePath );
812✔
2902
  }
993✔
2903

2904
  // go through files listed on the server, but not available locally
2905
  for ( MerginFile file : newServerFilesMap )
669✔
2906
  {
2907
    bool hasOldServer = oldServerFilesMap.contains( file.path );
388✔
2908

2909
    if ( hasOldServer )
388✔
2910
    {
2911
      if ( oldServerFilesMap.value( file.path ).checksum == file.checksum )
179✔
2912
      {
2913
        // L-D
2914
        if ( allowConfig )
179✔
2915
        {
2916
          bool shouldBeExcludedFromSync = MerginApi::excludeFromSync( file.path, config );
177✔
2917
          if ( shouldBeExcludedFromSync )
177✔
2918
          {
2919
            continue;
155✔
2920
          }
2921

2922
          // check if we should download missing files that were previously ignored (e.g. selective sync has been disabled)
2923
          bool previouslyIgnoredButShouldDownload = \
2924
              config.downloadMissingFiles &&
41✔
2925
              lastSyncConfig.isValid &&
41✔
2926
              MerginApi::excludeFromSync( file.path, lastSyncConfig );
19✔
2927

2928
          if ( previouslyIgnoredButShouldDownload )
22✔
2929
          {
2930
            diff.remoteAdded << file.path;
19✔
2931
            continue;
19✔
2932
          }
2933
        }
2934
        diff.localDeleted << file.path;
5✔
2935
      }
2936
      else
2937
      {
2938
        // C/R-U/L-D
2939
        diff.conflictRemoteUpdatedLocalDeleted << file.path;
×
2940
      }
2941
    }
2942
    else
2943
    {
2944
      // R-A
2945
      if ( allowConfig )
209✔
2946
      {
2947
        if ( MerginApi::excludeFromSync( file.path, config ) )
167✔
2948
        {
2949
          continue;
35✔
2950
        }
2951
      }
2952
      diff.remoteAdded << file.path;
174✔
2953
    }
2954

2955
    if ( hasOldServer )
179✔
2956
      oldServerFilesMap.remove( file.path );
5✔
2957
  }
388✔
2958

2959
  for ( MerginFile file : oldServerFilesMap )
455✔
2960
  {
2961
    // R-D/L-D
2962
    // TODO: need to do anything?
2963
  }
174✔
2964

2965
  return diff;
562✔
2966
}
281✔
2967

2968
MerginProject MerginApi::parseProjectMetadata( const QJsonObject &proj )
286✔
2969
{
2970
  MerginProject project;
286✔
2971

2972
  if ( proj.isEmpty() )
286✔
2973
  {
2974
    return project;
×
2975
  }
2976

2977
  if ( proj.contains( QStringLiteral( "error" ) ) )
286✔
2978
  {
2979
    // handle project error (user might be logged out / do not have write rights / project is on different server / project is orphaned)
2980
    project.remoteError = QString::number( proj.value( QStringLiteral( "error" ) ).toInt( 0 ) ); // error code
×
2981
    return project;
×
2982
  }
2983

2984
  project.projectName = proj.value( QStringLiteral( "name" ) ).toString();
286✔
2985
  project.projectNamespace = proj.value( QStringLiteral( "namespace" ) ).toString();
286✔
2986

2987
  QString versionStr = proj.value( QStringLiteral( "version" ) ).toString();
572✔
2988
  if ( versionStr.isEmpty() )
286✔
2989
  {
2990
    project.serverVersion = 0;
×
2991
  }
2992
  else if ( versionStr.startsWith( "v" ) ) // cut off 'v' part from v123
286✔
2993
  {
2994
    versionStr = versionStr.mid( 1 );
286✔
2995
    project.serverVersion = versionStr.toInt();
286✔
2996
  }
2997

2998
  QDateTime updated = QDateTime::fromString( proj.value( QStringLiteral( "updated" ) ).toString(), Qt::ISODateWithMs ).toUTC();
572✔
2999
  if ( !updated.isValid() )
286✔
3000
  {
3001
    project.serverUpdated = QDateTime::fromString( proj.value( QStringLiteral( "created" ) ).toString(), Qt::ISODateWithMs ).toUTC();
×
3002
  }
3003
  else
3004
  {
3005
    project.serverUpdated = updated;
286✔
3006
  }
3007
  return project;
286✔
3008
}
286✔
3009

3010

3011
MerginProjectsList MerginApi::parseProjectsFromJson( const QJsonDocument &doc )
29✔
3012
{
3013
  if ( !doc.isObject() )
29✔
3014
    return MerginProjectsList();
×
3015

3016
  QJsonObject object = doc.object();
29✔
3017
  MerginProjectsList result;
29✔
3018

3019
  if ( object.contains( "projects" ) && object.value( "projects" ).isArray() ) // listProjects API
29✔
3020
  {
3021
    QJsonArray vArray = object.value( "projects" ).toArray();
44✔
3022

3023
    for ( auto it = vArray.constBegin(); it != vArray.constEnd(); ++it )
195✔
3024
    {
3025
      result << parseProjectMetadata( it->toObject() );
173✔
3026
    }
3027
  }
22✔
3028
  else if ( !object.isEmpty() ) // listProjectsbyName API returns projects as separate objects not in array
7✔
3029
  {
3030
    for ( auto it = object.begin(); it != object.end(); ++it )
120✔
3031
    {
3032
      MerginProject project = parseProjectMetadata( it->toObject() );
113✔
3033
      if ( !project.remoteError.isEmpty() )
113✔
3034
      {
3035
        // add project namespace/name from object name in case of error
3036
        MerginApi::extractProjectName( it.key(), project.projectNamespace, project.projectName );
×
3037
      }
3038
      result << project;
113✔
3039
    }
113✔
3040
  }
3041
  return result;
29✔
3042
}
29✔
3043

3044
void MerginApi::refreshAuthToken()
7✔
3045
{
3046
  if ( !mUserAuth->hasAuthData() ||
14✔
3047
       mUserAuth->authToken().isEmpty() )
14✔
3048
  {
3049
    CoreUtils::log( QStringLiteral( "Auth" ), QStringLiteral( "Can not refresh token, missing credentials" ) );
×
3050
    return;
×
3051
  }
3052

3053
  if ( mUserAuth->tokenExpiration() < QDateTime::currentDateTimeUtc() )
7✔
3054
  {
3055
    CoreUtils::log( QStringLiteral( "Auth" ), QStringLiteral( "Token has expired, requesting new one" ) );
×
3056
    authorize( mUserAuth->username(), mUserAuth->password() );
×
3057
    mAuthLoopEvent.exec();
×
3058
  }
3059
}
3060

3061
QStringList MerginApi::generateChunkIdsForSize( qint64 fileSize )
163✔
3062
{
3063
  qreal rawNoOfChunks = qreal( fileSize ) / UPLOAD_CHUNK_SIZE;
163✔
3064
  int noOfChunks = qCeil( rawNoOfChunks );
163✔
3065

3066
  // edge case when file is empty, filesize equals zero
3067
  // manually set one chunk so that file will be synced
3068
  if ( fileSize <= 0 )
163✔
3069
    noOfChunks = 1;
45✔
3070

3071
  QStringList chunks;
163✔
3072
  for ( int i = 0; i < noOfChunks; i++ )
328✔
3073
  {
3074
    QString chunkID = CoreUtils::uuidWithoutBraces( QUuid::createUuid() );
165✔
3075
    chunks.append( chunkID );
165✔
3076
  }
165✔
3077
  return chunks;
163✔
3078
}
×
3079

3080
QJsonArray MerginApi::prepareUploadChangesJSON( const QList<MerginFile> &files )
345✔
3081
{
3082
  QJsonArray jsonArray;
345✔
3083

3084
  for ( MerginFile file : files )
500✔
3085
  {
3086
    QJsonObject fileObject;
155✔
3087
    fileObject.insert( "path", file.path );
155✔
3088

3089
    fileObject.insert( "size", file.size );
155✔
3090
    fileObject.insert( "mtime", file.mtime.toString( Qt::ISODateWithMs ) );
155✔
3091

3092
    if ( !file.diffName.isEmpty() )
155✔
3093
    {
3094
      // doing diff-based upload
3095
      QJsonObject diffObject;
12✔
3096
      diffObject.insert( "path", file.diffName );
12✔
3097
      diffObject.insert( "checksum", file.diffChecksum );
12✔
3098
      diffObject.insert( "size", file.diffSize );
12✔
3099

3100
      fileObject.insert( "diff", diffObject );
12✔
3101
      fileObject.insert( "checksum", file.diffBaseChecksum );
12✔
3102
    }
12✔
3103
    else
3104
    {
3105
      fileObject.insert( "checksum", file.checksum );
143✔
3106
    }
3107

3108
    QJsonArray chunksJson;
155✔
3109
    for ( QString id : file.chunks )
308✔
3110
    {
3111
      chunksJson.append( id );
153✔
3112
    }
153✔
3113
    fileObject.insert( "chunks", chunksJson );
155✔
3114
    jsonArray.append( fileObject );
155✔
3115
  }
155✔
3116
  return jsonArray;
345✔
3117
}
×
3118

3119
void MerginApi::finishProjectSync( const QString &projectFullName, bool syncSuccessful )
243✔
3120
{
3121
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
243✔
3122
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
243✔
3123

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

3126
  if ( syncSuccessful )
243✔
3127
  {
3128
    // update the local metadata file
3129
    writeData( transaction.projectMetadata, transaction.projectDir + "/" + MerginApi::sMetadataFile );
238✔
3130

3131
    // update info of local projects
3132
    mLocalProjects.updateLocalVersion( transaction.projectDir, transaction.version );
238✔
3133

3134
    CoreUtils::log( "sync " + projectFullName, QStringLiteral( "### Finished ###  New project version: %1\n" ).arg( transaction.version ) );
238✔
3135
  }
3136
  else
3137
  {
3138
    CoreUtils::log( "sync " + projectFullName, QStringLiteral( "### FAILED ###\n" ) );
5✔
3139
  }
3140

3141
  bool pullBeforePush = transaction.pullBeforePush;
243✔
3142
  QString projectDir = transaction.projectDir;  // keep it before the transaction gets removed
243✔
3143
  ProjectDiff diff = transaction.diff;
243✔
3144
  int newVersion = syncSuccessful ? transaction.version : -1;
243✔
3145

3146
  if ( transaction.gpkgSchemaChanged || projectFileHasBeenUpdated( diff ) )
243✔
3147
  {
3148
    emit projectReloadNeededAfterSync( projectFullName );
96✔
3149
  }
3150

3151
  mTransactionalStatus.remove( projectFullName );
243✔
3152

3153
  if ( pullBeforePush )
243✔
3154
  {
3155
    CoreUtils::log( "sync " + projectFullName, QStringLiteral( "Continue with push after pull" ) );
26✔
3156
    // we're done only with the download part before the actual upload - so let's continue with upload
3157
    QString projectNamespace, projectName;
26✔
3158
    extractProjectName( projectFullName, projectNamespace, projectName );
26✔
3159
    pushProject( projectNamespace, projectName );
26✔
3160
  }
26✔
3161
  else
3162
  {
3163
    emit syncProjectFinished( projectFullName, syncSuccessful, newVersion );
217✔
3164

3165
    if ( syncSuccessful )
217✔
3166
    {
3167
      emit projectDataChanged( projectFullName );
212✔
3168
    }
3169
  }
3170
}
243✔
3171

3172
bool MerginApi::writeData( const QByteArray &data, const QString &path )
238✔
3173
{
3174
  QFile file( path );
238✔
3175
  createPathIfNotExists( path );
238✔
3176
  if ( !file.open( QIODevice::WriteOnly ) )
238✔
3177
  {
3178
    return false;
×
3179
  }
3180

3181
  file.write( data );
238✔
3182
  file.close();
238✔
3183

3184
  return true;
238✔
3185
}
238✔
3186

3187

3188
void MerginApi::createPathIfNotExists( const QString &filePath )
706✔
3189
{
3190
  QDir dir;
706✔
3191
  if ( !dir.exists( mDataDir ) )
706✔
3192
    dir.mkpath( mDataDir );
×
3193

3194
  QFileInfo newFile( filePath );
706✔
3195
  if ( !newFile.absoluteDir().exists() )
706✔
3196
  {
3197
    if ( !dir.mkpath( newFile.absolutePath() ) )
196✔
3198
    {
3199
      CoreUtils::log( "create path", QString( "Creating a folder failed for path: %1" ).arg( filePath ) );
×
3200
    }
3201
  }
3202
}
706✔
3203

3204
bool MerginApi::isInIgnore( const QFileInfo &info )
2,469✔
3205
{
3206
  return sIgnoreExtensions.contains( info.suffix() ) || sIgnoreFiles.contains( info.fileName() ) || info.filePath().contains( sMetadataFolder + "/" );
2,469✔
3207
}
3208

3209
bool MerginApi::excludeFromSync( const QString &filePath, const MerginConfig &config )
377✔
3210
{
3211
  if ( config.isValid && config.selectiveSyncEnabled )
377✔
3212
  {
3213
    QFileInfo info( filePath );
247✔
3214

3215
    bool isExcludedFormat = sIgnoreImageExtensions.contains( info.suffix().toLower() );
247✔
3216

3217
    if ( !isExcludedFormat )
247✔
3218
      return false;
20✔
3219

3220
    if ( config.selectiveSyncDir.isEmpty() )
227✔
3221
    {
3222
      return true; // we are ignoring photos in the entire project
98✔
3223
    }
3224
    else if ( filePath.startsWith( config.selectiveSyncDir ) )
129✔
3225
    {
3226
      return true; // we are ignoring photo in subfolder
121✔
3227
    }
3228
  }
247✔
3229
  return false;
138✔
3230
}
3231

3232
QByteArray MerginApi::getChecksum( const QString &filePath )
1,009✔
3233
{
3234
  QFile f( filePath );
1,009✔
3235
  if ( f.open( QFile::ReadOnly ) )
1,009✔
3236
  {
3237
    QCryptographicHash hash( QCryptographicHash::Sha1 );
1,009✔
3238
    QByteArray chunk = f.read( CHUNK_SIZE );
1,009✔
3239
    while ( !chunk.isEmpty() )
2,596✔
3240
    {
3241
      hash.addData( chunk );
1,587✔
3242
      chunk = f.read( CHUNK_SIZE );
1,587✔
3243
    }
3244
    f.close();
1,009✔
3245
    return hash.result().toHex();
2,018✔
3246
  }
1,009✔
3247

3248
  return QByteArray();
×
3249
}
1,009✔
3250

3251
QSet<QString> MerginApi::listFiles( const QString &path )
281✔
3252
{
3253
  QSet<QString> files;
281✔
3254
  QDirIterator it( path, QStringList() << QStringLiteral( "*" ), QDir::Files, QDirIterator::Subdirectories );
843✔
3255
  while ( it.hasNext() )
1,274✔
3256
  {
3257
    it.next();
993✔
3258
    if ( !isInIgnore( it.fileInfo() ) )
993✔
3259
    {
3260
      files << it.filePath().replace( path, "" );
993✔
3261
    }
3262
  }
3263
  return files;
562✔
3264
}
281✔
3265

3266
void MerginApi::deleteAccount()
1✔
3267
{
3268
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
1✔
3269
  {
3270
    return;
×
3271
  }
3272

3273
  QNetworkRequest request = getDefaultRequest();
1✔
3274
  QUrl url( mApiRoot + QStringLiteral( "/v1/user" ) );
2✔
3275
  request.setUrl( url );
1✔
3276
  QNetworkReply *reply = mManager.deleteResource( request );
1✔
3277
  connect( reply, &QNetworkReply::finished, this, [this]() { this->deleteAccountFinished();} );
2✔
3278
  CoreUtils::log( "delete account " + mUserAuth->username(), QStringLiteral( "Requesting account deletion: " ) + url.toString() );
2✔
3279
}
1✔
3280

3281
void MerginApi::deleteAccountFinished()
1✔
3282
{
3283
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
1✔
3284
  Q_ASSERT( r );
1✔
3285

3286
  if ( r->error() == QNetworkReply::NoError )
1✔
3287
  {
3288
    CoreUtils::log( "delete account " + mUserAuth->username(), QStringLiteral( "Success" ) );
1✔
3289

3290
    // remove all local projects from the device
3291
    LocalProjectsList projects = mLocalProjects.projects();
1✔
3292
    for ( const LocalProject &info : projects )
36✔
3293
    {
3294
      mLocalProjects.removeLocalProject( info.id() );
35✔
3295
    }
3296

3297
    clearAuth();
1✔
3298

3299
    emit accountDeleted( true );
1✔
3300
  }
1✔
3301
  else
3302
  {
3303
    int statusCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
×
3304
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
3305
    CoreUtils::log( "delete account " + mUserAuth->username(), QStringLiteral( "FAILED - %1 %2. %3" ).arg( statusCode ).arg( r->errorString() ).arg( serverMsg ) );
×
3306
    if ( statusCode == 422 )
×
3307
    {
3308
      emit userIsAnOrgOwnerError();
×
3309
    }
3310
    else
3311
    {
3312
      emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: deleteAccount" ) );
×
3313
    }
3314

3315
    emit accountDeleted( false );
×
3316
  }
×
3317

3318
  r->deleteLater();
1✔
3319
}
1✔
3320

3321
void MerginApi::getServerConfig()
24✔
3322
{
3323
  QNetworkRequest request = getDefaultRequest();
24✔
3324
  QString urlString = mApiRoot + QStringLiteral( "/config" );
48✔
3325
  QUrl url( urlString );
24✔
3326
  request.setUrl( url );
24✔
3327

3328
  QNetworkReply *reply = mManager.get( request );
24✔
3329

3330
  connect( reply, &QNetworkReply::finished, this, &MerginApi::getServerConfigReplyFinished );
24✔
3331
  CoreUtils::log( "Config", QStringLiteral( "Requesting server configuration: " ) + url.toString() );
48✔
3332
}
24✔
3333

3334
void MerginApi::getServerConfigReplyFinished()
12✔
3335
{
3336
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
12✔
3337
  Q_ASSERT( r );
12✔
3338

3339
  if ( r->error() == QNetworkReply::NoError )
12✔
3340
  {
3341
    CoreUtils::log( "Config", QStringLiteral( "Success" ) );
12✔
3342
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
12✔
3343
    if ( doc.isObject() )
12✔
3344
    {
3345
      QString serverType = doc.object().value( QStringLiteral( "server_type" ) ).toString();
36✔
3346
      if ( serverType == QStringLiteral( "ee" ) )
12✔
3347
      {
3348
        setServerType( MerginServerType::EE );
×
3349
      }
3350
      else if ( serverType == QStringLiteral( "ce" ) )
12✔
3351
      {
3352
        setServerType( MerginServerType::CE );
×
3353
      }
3354
      else if ( serverType == QStringLiteral( "saas" ) )
12✔
3355
      {
3356
        setServerType( MerginServerType::SAAS );
12✔
3357
      }
3358
    }
12✔
3359
  }
12✔
3360
  else
3361
  {
3362
    int statusCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
×
3363
    if ( statusCode == 404 ) // legacy (old) server
×
3364
    {
3365
      setServerType( MerginServerType::OLD );
×
3366
    }
3367
    else
3368
    {
3369
      QString serverMsg = extractServerErrorMsg( r->readAll() );
×
3370
      QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "getServerType" ), r->errorString(), serverMsg );
×
3371
      emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: getServerType" ) );
×
3372
      CoreUtils::log( "server type", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
3373
    }
×
3374
  }
3375

3376
  r->deleteLater();
12✔
3377
}
12✔
3378

3379
MerginServerType::ServerType MerginApi::serverType() const
13✔
3380
{
3381
  return mServerType;
13✔
3382
}
3383

3384
void MerginApi::setServerType( const MerginServerType::ServerType &serverType )
17✔
3385
{
3386
  if ( mServerType != serverType )
17✔
3387
  {
3388
    if ( mServerType == MerginServerType::OLD && serverType == MerginServerType::SAAS )
6✔
3389
    {
3390
      emit serverWasUpgraded();
2✔
3391
    }
3392

3393
    mServerType = serverType;
6✔
3394
    QSettings settings;
6✔
3395
    settings.beginGroup( QStringLiteral( "Input/" ) );
6✔
3396
    settings.setValue( QStringLiteral( "serverType" ), mServerType );
12✔
3397
    settings.endGroup();
6✔
3398
    emit serverTypeChanged();
6✔
3399
    emit apiSupportsWorkspacesChanged();
6✔
3400
  }
6✔
3401
}
17✔
3402

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

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

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

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

3425
  if ( r->error() == QNetworkReply::NoError )
×
3426
  {
3427
    CoreUtils::log( "list workspaces", QStringLiteral( "Success" ) );
×
3428
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
×
3429
    if ( doc.isArray() )
×
3430
    {
3431
      QMap<int, QString> workspaces;
×
3432
      QJsonArray array = doc.array();
×
3433
      for ( auto it = array.constBegin(); it != array.constEnd(); ++it )
×
3434
      {
3435
        QJsonObject ws = it->toObject();
×
3436
        workspaces.insert( ws.value( QStringLiteral( "id" ) ).toInt(), ws.value( QStringLiteral( "name" ) ).toString() );
×
3437
      }
×
3438

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

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

3459
void MerginApi::listInvitations()
×
3460
{
3461
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
×
3462
  {
3463
    emit listInvitationsFailed();
×
3464
    return;
×
3465
  }
3466

3467
  QUrl url( mApiRoot + QStringLiteral( "/v1/workspace/invitations" ) );
×
3468
  QNetworkRequest request = getDefaultRequest( mUserAuth->hasAuthData() );
×
3469
  request.setUrl( url );
×
3470

3471
  QNetworkReply *reply = mManager.get( request );
×
3472
  CoreUtils::log( "list invitations", QStringLiteral( "Requesting: " ) + url.toString() );
×
3473
  connect( reply, &QNetworkReply::finished, this, &MerginApi::listInvitationsReplyFinished );
×
3474
}
×
3475

3476
void MerginApi::listInvitationsReplyFinished()
×
3477
{
3478
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
×
3479
  Q_ASSERT( r );
×
3480

3481
  if ( r->error() == QNetworkReply::NoError )
×
3482
  {
3483
    CoreUtils::log( "list invitations", QStringLiteral( "Success" ) );
×
3484
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
×
3485
    if ( doc.isArray() )
×
3486
    {
3487
      QList<MerginInvitation> invitations;
×
3488
      QJsonArray array = doc.array();
×
3489
      for ( auto it = array.constBegin(); it != array.constEnd(); ++it )
×
3490
      {
3491
        MerginInvitation invite = MerginInvitation::fromJsonObject( it->toObject() );
×
3492
        invitations.append( invite );
×
3493
      }
×
3494

3495
      emit listInvitationsFinished( invitations );
×
3496
    }
×
3497
    else
3498
    {
3499
      emit listInvitationsFailed();
×
3500
    }
3501
  }
×
3502
  else
3503
  {
3504
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
3505
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listInvitations" ), r->errorString(), serverMsg );
×
3506
    CoreUtils::log( "list invitations", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
3507
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: listInvitations" ) );
×
3508
    emit listInvitationsFailed();
×
3509
  }
×
3510

3511
  r->deleteLater();
×
3512
}
×
3513

3514
void MerginApi::processInvitation( const QString &uuid, bool accept )
×
3515
{
3516
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
×
3517
  {
3518
    emit processInvitationFailed();
×
3519
    return;
×
3520
  }
3521

3522
  QNetworkRequest request = getDefaultRequest( true );
×
3523
  QString urlString = mApiRoot + QStringLiteral( "v1/workspace/invitation/%1" ).arg( uuid );
×
3524
  QUrl url( urlString );
×
3525
  request.setUrl( url );
×
3526
  request.setRawHeader( "Content-Type", "application/json" );
×
3527
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrAcceptFlag ), accept );
×
3528

3529
  QJsonDocument jsonDoc;
×
3530
  QJsonObject jsonObject;
×
3531
  jsonObject.insert( QStringLiteral( "accept" ), accept );
×
3532
  jsonDoc.setObject( jsonObject );
×
3533
  QByteArray json = jsonDoc.toJson( QJsonDocument::Compact );
×
3534
  QNetworkReply *reply = mManager.post( request, json );
×
3535
  CoreUtils::log( "process invitation", QStringLiteral( "Requesting: " ) + url.toString() );
×
3536
  connect( reply, &QNetworkReply::finished, this, &MerginApi::processInvitationReplyFinished );
×
3537
}
×
3538

3539
void MerginApi::processInvitationReplyFinished()
×
3540
{
3541
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
×
3542
  Q_ASSERT( r );
×
3543

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

3546
  if ( r->error() == QNetworkReply::NoError )
×
3547
  {
3548
    CoreUtils::log( "process invitation", QStringLiteral( "Success" ) );
×
3549
  }
3550
  else
3551
  {
3552
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
3553
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "processInvitation" ), r->errorString(), serverMsg );
×
3554
    CoreUtils::log( "process invitation", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
3555
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: processInvitation" ) );
×
3556
    emit processInvitationFailed();
×
3557
  }
×
3558

3559
  emit processInvitationFinished( accept );
×
3560

3561
  r->deleteLater();
×
3562
}
×
3563

3564
bool MerginApi::createWorkspace( const QString &workspaceName )
2✔
3565
{
3566
  if ( !validateAuth() )
2✔
3567
  {
3568
    emit missingAuthorizationError( workspaceName );
×
3569
    return false;
×
3570
  }
3571

3572
  if ( mApiVersionStatus != MerginApiStatus::OK )
2✔
3573
  {
3574
    return false;
×
3575
  }
3576

3577
  if ( !CoreUtils::isValidName( workspaceName ) )
2✔
3578
  {
3579
    emit notify( tr( "Workspace name contains invalid characters" ) );
×
3580
    return false;
×
3581
  }
3582

3583
  QNetworkRequest request = getDefaultRequest();
2✔
3584
  QUrl url( mApiRoot + QString( "/v1/workspace" ) );
4✔
3585
  request.setUrl( url );
2✔
3586
  request.setRawHeader( "Content-Type", "application/json" );
2✔
3587
  request.setRawHeader( "Accept", "application/json" );
2✔
3588
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrWorkspaceName ), workspaceName );
2✔
3589

3590
  QJsonDocument jsonDoc;
2✔
3591
  QJsonObject jsonObject;
2✔
3592
  jsonObject.insert( QStringLiteral( "name" ), workspaceName );
4✔
3593
  jsonDoc.setObject( jsonObject );
2✔
3594
  QByteArray json = jsonDoc.toJson( QJsonDocument::Compact );
2✔
3595

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

3600
  return true;
2✔
3601
}
2✔
3602

3603
void MerginApi::signOut()
×
3604
{
3605
  clearAuth();
×
3606
}
×
3607

3608
void MerginApi::refreshUserData()
×
3609
{
3610
  getUserInfo();
×
3611

3612
  if ( apiSupportsWorkspaces() )
×
3613
  {
3614
    getWorkspaceInfo();
×
3615
    // getServiceInfo is called automatically when workspace info finishes
3616
  }
3617
  else if ( mServerType == MerginServerType::OLD )
×
3618
  {
3619
    getServiceInfo();
×
3620
  }
3621
}
×
3622

3623
void MerginApi::createWorkspaceReplyFinished()
2✔
3624
{
3625
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
2✔
3626
  Q_ASSERT( r );
2✔
3627

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

3630
  if ( r->error() == QNetworkReply::NoError )
2✔
3631
  {
3632
    CoreUtils::log( "create " + workspaceName, QStringLiteral( "Success" ) );
2✔
3633
    emit workspaceCreated( workspaceName, true );
2✔
3634
  }
3635
  else
3636
  {
3637
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
3638
    QString message = QStringLiteral( "FAILED - %1: %2" ).arg( r->errorString(), serverMsg );
×
3639
    CoreUtils::log( "create " + workspaceName, message );
×
3640

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

3643
    if ( httpCode == 409 )
×
3644
    {
3645
      emit networkErrorOccurred( tr( "Workspace %1 already exists" ).arg( workspaceName ), QStringLiteral( "Mergin API error: createWorkspace" ), httpCode, workspaceName );
×
3646
    }
3647
    else
3648
    {
3649
      emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: createWorkspace" ), httpCode, workspaceName );
×
3650
    }
3651
    emit workspaceCreated( workspaceName, false );
×
3652
  }
×
3653
  r->deleteLater();
2✔
3654
}
2✔
3655

3656
bool MerginApi::apiSupportsWorkspaces()
×
3657
{
3658
  if ( mServerType == MerginServerType::SAAS || mServerType == MerginServerType::EE )
×
3659
  {
3660
    return true;
×
3661
  }
3662
  else
3663
  {
3664
    return false;
×
3665
  }
3666
}
3667

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