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

MerginMaps / input / 5290081864

pending completion
5290081864

push

github

web-flow
Merge pull request #2715 from MerginMaps/dev-2.3_CU-2ub0eca_ZoomAndRotation

Added zoom and rotation to pictures in feature form

8116 of 13074 relevant lines covered (62.08%)

106.33 hits per line

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

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

10
#include "merginapi.h"
11

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

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

26
#include <geodiff.h>
27

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

38

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

60
  qRegisterMetaType<Transactions>();
22✔
61

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

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

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

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

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

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

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

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

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

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

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

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

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

152
  QUrlQuery query;
22✔
153

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

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

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

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

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

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

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

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

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

201
  return requestId;
22✔
202
}
22✔
203

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

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

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

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

225
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/by_names" ) );
7✔
226

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

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

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

237
  return requestId;
7✔
238
}
7✔
239

240

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

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

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

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

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

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

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

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

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

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

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

301
  return request;
1,070✔
302
}
1,070✔
303

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

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

318
  return false;
×
319
}
×
320

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

482

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

594

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

686
  return pullHasStarted;
100✔
687
}
100✔
688

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

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

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

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

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

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

719
  return pushHasStarted;
143✔
720
}
143✔
721

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

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

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

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

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

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

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

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

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

792
  }
1✔
793

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

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

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

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

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

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

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

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

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

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

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

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

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

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

888
  QString urlString;
32✔
889

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

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

907
  QNetworkReply *reply = mManager.get( request );
32✔
908

909
  connect( reply, &QNetworkReply::finished, this, &MerginApi::getServiceInfoReplyFinished );
32✔
910

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

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

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

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

936
    mSubscriptionInfo->clear();
×
937

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

953
  r->deleteLater();
31✔
954
}
31✔
955

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

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

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

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

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

996
  QString projectFullName = getFullProjectName( projectNamespace, projectName );
40✔
997

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

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

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

1016
  return true;
40✔
1017
}
40✔
1018

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

1026
  QString projectFullName = getFullProjectName( projectNamespace, projectName );
43✔
1027

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

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

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

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

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

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

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

1063

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

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

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

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

1094
void MerginApi::deleteProjectFinished( bool informUser )
43✔
1095
{
1096
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
43✔
1097
  Q_ASSERT( r );
43✔
1098

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

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

1105
    if ( informUser )
2✔
1106
      emit notify( QStringLiteral( "Project deleted" ) );
2✔
1107

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

1120
void MerginApi::authorizeFinished()
8✔
1121
{
1122
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
8✔
1123
  Q_ASSERT( r );
8✔
1124

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

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

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

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

1187
    if ( !username.isEmpty() && !password.isEmpty() ) // log in immediately
3✔
1188
      authorize( username, password );
3✔
1189

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

1220
void MerginApi::pingMerginReplyFinished()
10✔
1221
{
1222
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
10✔
1223
  Q_ASSERT( r );
10✔
1224
  QString apiVersion;
10✔
1225
  QString serverMsg;
10✔
1226
  bool serverSupportsSubscriptions = false;
10✔
1227

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

1248
void MerginApi::onPlanProductIdChanged()
14✔
1249
{
1250
  if ( mUserAuth->hasAuthData() )
14✔
1251
  {
1252
    if ( mServerType == MerginServerType::OLD )
13✔
1253
    {
1254
      getUserInfo();
×
1255
    }
×
1256
    else
1257
    {
1258
      getWorkspaceInfo();
13✔
1259
    }
1260
  }
13✔
1261
}
14✔
1262

1263
QNetworkReply *MerginApi::getProjectInfo( const QString &projectFullName, bool withAuth )
243✔
1264
{
1265
  if ( withAuth && !validateAuth() )
243✔
1266
  {
1267
    emit missingAuthorizationError( projectFullName );
×
1268
    return nullptr;
×
1269
  }
1270

1271
  if ( mApiVersionStatus != MerginApiStatus::OK )
243✔
1272
  {
1273
    return nullptr;
×
1274
  }
1275

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

1285
  QUrlQuery query;
243✔
1286
  if ( sinceVersion != -1 )
243✔
1287
    query.addQueryItem( QStringLiteral( "since" ), QStringLiteral( "v%1" ).arg( sinceVersion ) );
180✔
1288

1289
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/%1" ).arg( projectFullName ) );
243✔
1290
  url.setQuery( query );
243✔
1291

1292
  QNetworkRequest request = getDefaultRequest( withAuth );
243✔
1293
  request.setUrl( url );
243✔
1294
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
243✔
1295

1296
  return mManager.get( request );
243✔
1297
}
243✔
1298

1299
bool MerginApi::validateAuth()
811✔
1300
{
1301
  if ( !mUserAuth->hasAuthData() )
811✔
1302
  {
1303
    emit authRequested();
×
1304
    return false;
×
1305
  }
1306

1307
  if ( mUserAuth->authToken().isEmpty() || mUserAuth->tokenExpiration() < QDateTime().currentDateTime().toUTC() )
811✔
1308
  {
1309
    authorize( mUserAuth->username(), mUserAuth->password() );
1✔
1310
    CoreUtils::log( QStringLiteral( "MerginApi" ), QStringLiteral( "Requesting authorization because of missing or expired token." ) );
1✔
1311
    mAuthLoopEvent.exec();
1✔
1312
  }
1✔
1313
  return true;
811✔
1314
}
811✔
1315

1316
void MerginApi::checkMerginVersion( QString apiVersion, bool serverSupportsSubscriptions, QString msg )
10✔
1317
{
1318
  setApiSupportsSubscriptions( serverSupportsSubscriptions );
10✔
1319

1320
  if ( msg.isEmpty() )
10✔
1321
  {
1322
    int major = -1;
10✔
1323
    int minor = -1;
10✔
1324
    QRegularExpression re;
10✔
1325
    re.setPattern( QStringLiteral( "(?<major>\\d+)[.](?<minor>\\d+)" ) );
10✔
1326
    QRegularExpressionMatch match = re.match( apiVersion );
10✔
1327
    if ( match.hasMatch() )
10✔
1328
    {
1329
      major = match.captured( "major" ).toInt();
10✔
1330
      minor = match.captured( "minor" ).toInt();
10✔
1331
    }
10✔
1332

1333
    if ( ( MERGIN_API_VERSION_MAJOR == major && MERGIN_API_VERSION_MINOR <= minor ) || ( MERGIN_API_VERSION_MAJOR < major ) )
10✔
1334
    {
1335
      setApiVersionStatus( MerginApiStatus::OK );
10✔
1336
    }
10✔
1337
    else
1338
    {
1339
      setApiVersionStatus( MerginApiStatus::INCOMPATIBLE );
×
1340
    }
1341
  }
10✔
1342
  else
1343
  {
1344
    setApiVersionStatus( MerginApiStatus::NOT_FOUND );
×
1345
  }
1346
}
10✔
1347

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

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

1408
  return serverMsg;
49✔
1409
}
49✔
1410

1411

1412
LocalProject MerginApi::getLocalProject( const QString &projectFullName )
7✔
1413
{
1414
  return mLocalProjects.projectFromMerginName( projectFullName );
7✔
1415
}
1416

1417
ProjectDiff MerginApi::localProjectChanges( const QString &projectDir )
39✔
1418
{
1419
  MerginProjectMetadata projectMetadata = MerginProjectMetadata::fromCachedJson( projectDir + "/" + sMetadataFile );
39✔
1420
  QList<MerginFile> localFiles = getLocalProjectFiles( projectDir + "/" );
39✔
1421

1422
  MerginConfig config = MerginConfig::fromFile( projectDir + "/" + sMerginConfigFile );
39✔
1423

1424
  return compareProjectFiles( projectMetadata.files, projectMetadata.files, localFiles, projectDir, config.isValid, config );
39✔
1425
}
39✔
1426

1427
QString MerginApi::getTempProjectDir( const QString &projectFullName )
337✔
1428
{
1429
  return mDataDir + "/" + TEMP_FOLDER + projectFullName;
337✔
1430
}
×
1431

1432
QString MerginApi::getFullProjectName( QString projectNamespace, QString projectName ) // TODO: move to inpututils?
42,958✔
1433
{
1434
  return QString( "%1/%2" ).arg( projectNamespace ).arg( projectName );
42,958✔
1435
}
×
1436

1437
MerginApiStatus::VersionStatus MerginApi::apiVersionStatus() const
33✔
1438
{
1439
  return mApiVersionStatus;
33✔
1440
}
1441

1442
void MerginApi::setApiVersionStatus( const MerginApiStatus::VersionStatus &apiVersionStatus )
33✔
1443
{
1444
  if ( mApiVersionStatus != apiVersionStatus )
33✔
1445
  {
1446
    mApiVersionStatus = apiVersionStatus;
31✔
1447
    emit apiVersionStatusChanged();
31✔
1448
  }
31✔
1449
}
33✔
1450

1451
void MerginApi::pingMergin()
23✔
1452
{
1453
  if ( mApiVersionStatus == MerginApiStatus::OK ) return;
23✔
1454

1455
  setApiVersionStatus( MerginApiStatus::PENDING );
23✔
1456

1457
  QNetworkRequest request = getDefaultRequest( false );
23✔
1458
  QUrl url( mApiRoot + QStringLiteral( "/ping" ) );
23✔
1459
  request.setUrl( url );
23✔
1460

1461
  QNetworkReply *reply = mManager.get( request );
23✔
1462
  CoreUtils::log( "ping", QStringLiteral( "Requesting: " ) + url.toString() );
23✔
1463
  connect( reply, &QNetworkReply::finished, this, &MerginApi::pingMerginReplyFinished );
23✔
1464
}
23✔
1465

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

1479
void MerginApi::detachProjectFromMergin( const QString &projectNamespace, const QString &projectName, bool informUser )
1✔
1480
{
1481
  // Remove mergin folder
1482
  QString projectFullName = getFullProjectName( projectNamespace, projectName );
1✔
1483
  LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName );
1✔
1484

1485
  if ( projectInfo.isValid() )
1✔
1486
  {
1487
    CoreUtils::removeDir( projectInfo.projectDir + "/.mergin" );
1✔
1488
  }
1✔
1489

1490
  // Update localProject
1491
  mLocalProjects.updateNamespace( projectInfo.projectDir, "" );
1✔
1492
  mLocalProjects.updateLocalVersion( projectInfo.projectDir, -1 );
1✔
1493

1494
  if ( informUser )
1✔
1495
    emit notify( tr( "Project detached from Mergin" ) );
1✔
1496

1497
  emit projectDetached( projectFullName );
1✔
1498
}
1✔
1499

1500
QString MerginApi::apiRoot() const
115✔
1501
{
1502
  return mApiRoot;
115✔
1503
}
1504

1505
void MerginApi::setApiRoot( const QString &apiRoot )
4✔
1506
{
1507
  QString newApiRoot;
4✔
1508
  if ( apiRoot.isEmpty() )
4✔
1509
  {
1510
    newApiRoot = defaultApiRoot();
×
1511
  }
×
1512
  else
1513
  {
1514
    newApiRoot = apiRoot;
4✔
1515
  }
1516

1517
  if ( newApiRoot != mApiRoot )
4✔
1518
  {
1519
    mApiRoot = newApiRoot;
2✔
1520

1521
    QSettings settings;
2✔
1522
    settings.setValue( QStringLiteral( "Input/apiRoot" ), mApiRoot );
2✔
1523

1524
    emit apiRootChanged();
2✔
1525
  }
2✔
1526
}
4✔
1527

1528
QString MerginApi::merginUserName() const
28✔
1529
{
1530
  return userAuth()->username();
28✔
1531
}
1532

1533
QList<MerginFile> MerginApi::getLocalProjectFiles( const QString &projectPath )
279✔
1534
{
1535
  QList<MerginFile> merginFiles;
279✔
1536
  QSet<QString> localFiles = listFiles( projectPath );
279✔
1537
  for ( QString p : localFiles )
1,268✔
1538
  {
1539

1540
    MerginFile file;
989✔
1541
    QByteArray localChecksumBytes = getChecksum( projectPath + p );
989✔
1542
    QString localChecksum = QString::fromLatin1( localChecksumBytes.data(), localChecksumBytes.size() );
989✔
1543
    file.checksum = localChecksum;
989✔
1544
    file.path = p;
989✔
1545
    QFileInfo info( projectPath + p );
989✔
1546
    file.size = info.size();
989✔
1547
    file.mtime = info.lastModified();
989✔
1548
    merginFiles.append( file );
989✔
1549
  }
989✔
1550
  return merginFiles;
279✔
1551
}
279✔
1552

1553
void MerginApi::listProjectsReplyFinished( QString requestId )
22✔
1554
{
1555
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
22✔
1556
  Q_ASSERT( r );
22✔
1557

1558
  int projectCount = -1;
22✔
1559
  int requestedPage = 1;
22✔
1560
  MerginProjectsList projectList;
22✔
1561

1562
  if ( r->error() == QNetworkReply::NoError )
22✔
1563
  {
1564
    QUrlQuery query( r->request().url().query() );
22✔
1565
    requestedPage = query.queryItemValue( "page" ).toInt();
22✔
1566

1567
    QByteArray data = r->readAll();
22✔
1568
    QJsonDocument doc = QJsonDocument::fromJson( data );
22✔
1569

1570
    if ( doc.isObject() )
22✔
1571
    {
1572
      projectCount = doc.object().value( "count" ).toInt();
22✔
1573
      projectList = parseProjectsFromJson( doc );
22✔
1574
    }
22✔
1575

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

1585
    emit listProjectsFailed();
×
1586
  }
×
1587

1588
  r->deleteLater();
22✔
1589

1590
  emit listProjectsFinished( projectList, projectCount, requestedPage, requestId );
22✔
1591
}
22✔
1592

1593
void MerginApi::listProjectsByNameReplyFinished( QString requestId )
7✔
1594
{
1595
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
7✔
1596
  Q_ASSERT( r );
7✔
1597

1598
  MerginProjectsList projectList;
7✔
1599

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

1614
    emit listProjectsFailed();
×
1615
  }
×
1616

1617
  r->deleteLater();
7✔
1618

1619
  emit listProjectsByNameFinished( projectList, requestId );
7✔
1620
}
7✔
1621

1622

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

1627
  QString dest = projectDir + "/" + filePath;
202✔
1628
  createPathIfNotExists( dest );
202✔
1629

1630
  QFile f( dest );
202✔
1631
  if ( !f.open( QIODevice::WriteOnly ) )
202✔
1632
  {
1633
    CoreUtils::log( "pull " + projectFullName, "Failed to open file for writing " + dest );
×
1634
    return;
×
1635
  }
1636

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

1649
  f.close();
202✔
1650

1651
  // if diffable, copy to .mergin dir so we have a basefile
1652
  if ( MerginApi::isFileDiffable( filePath ) )
202✔
1653
  {
1654
    QString basefile = projectDir + "/.mergin/" + filePath;
15✔
1655
    createPathIfNotExists( basefile );
15✔
1656

1657
    if ( !QFile::remove( basefile ) )
15✔
1658
    {
1659
      CoreUtils::log( "pull " + projectFullName, "failed to remove old basefile for: " + filePath );
15✔
1660
    }
15✔
1661
    if ( !QFile::copy( dest, basefile ) )
15✔
1662
    {
1663
      CoreUtils::log( "pull " + projectFullName, "failed to copy new basefile for: " + filePath );
×
1664
    }
×
1665
  }
15✔
1666
}
202✔
1667

1668

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

1673
  // update diffable files that have been modified on the server
1674
  // - if they were not modified locally, the server changes will be simply applied
1675
  // - if they were modified locally, local changes will be rebased on top of server changes
1676

1677
  QString src = tempDir + "/" + CoreUtils::uuidWithoutBraces( QUuid::createUuid() );
9✔
1678
  QString dest = projectDir + "/" + filePath;
9✔
1679
  QString basefile = projectDir + "/.mergin/" + filePath;
9✔
1680

1681
  LocalProject info = mLocalProjects.projectFromMerginName( projectFullName );
9✔
1682

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

1686
  createPathIfNotExists( src );
9✔
1687
  createPathIfNotExists( dest );
9✔
1688
  createPathIfNotExists( basefile );
9✔
1689

1690
  QStringList diffFiles;
9✔
1691
  for ( const auto &item : items )
19✔
1692
    diffFiles << tempDir + "/" + item.tempFileName;
10✔
1693

1694
  //
1695
  // let's first assemble server's file from our basefile + diffs
1696
  //
1697

1698
  if ( !QFile::copy( basefile, src ) )
9✔
1699
  {
1700
    CoreUtils::log( "pull " + projectFullName, "assemble server file fail: copying failed " + basefile + " to " + src );
×
1701

1702
    // TODO: this is a critical failure - we should abort pull
1703
  }
×
1704

1705
  if ( !GeodiffUtils::applyDiffs( src, diffFiles ) )
9✔
1706
  {
1707
    CoreUtils::log( "pull " + projectFullName, "server file assembly failed: " + filePath );
×
1708

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

1717
  //
1718
  // now we are ready for the update of our local file
1719
  //
1720
  bool hasConflicts = false;
9✔
1721

1722
  bool res = GeodiffUtils::rebase( basefile,
9✔
1723
                                   src,
1724
                                   dest,
1725
                                   conflictfile
1726
                                 );
1727
  if ( res )
9✔
1728
  {
1729
    CoreUtils::log( "pull " + projectFullName, "geodiff rebase successful: " + filePath );
8✔
1730
  }
8✔
1731
  else
1732
  {
1733
    CoreUtils::log( "pull " + projectFullName, "geodiff rebase failed! " + filePath );
1✔
1734

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

1750
  //
1751
  // finally update our basefile
1752
  //
1753

1754
  if ( !QFile::remove( basefile ) )
9✔
1755
  {
1756
    CoreUtils::log( "pull " + projectFullName, "failed removal of old basefile: " + filePath );
×
1757

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

1764
    // TODO: this is a critical failure - we should abort pull
1765
  }
×
1766
  return hasConflicts;
9✔
1767
}
9✔
1768

1769
void MerginApi::finalizeProjectPull( const QString &projectFullName )
123✔
1770
{
1771
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
123✔
1772
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
123✔
1773

1774
  QString projectDir = transaction.projectDir;
123✔
1775
  QString tempProjectDir = getTempProjectDir( projectFullName );
123✔
1776

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

1779
  for ( const PullTask &finalizationItem : transaction.pullTasks )
337✔
1780
  {
1781
    switch ( finalizationItem.method )
214✔
1782
    {
1783
      case PullTask::Copy:
1784
      {
1785
        finalizeProjectPullCopy( projectFullName, projectDir, tempProjectDir, finalizationItem.filePath, finalizationItem.data );
199✔
1786
        break;
199✔
1787
      }
1788

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

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

1816
      case PullTask::Delete:
1817
      {
1818
        CoreUtils::log( "pull " + projectFullName, "Removing local file: " + finalizationItem.filePath );
3✔
1819
        QFile file( projectDir + "/" + finalizationItem.filePath );
3✔
1820
        file.remove();
3✔
1821
        break;
1822
      }
3✔
1823
    }
1824

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

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

1840
  QDir( tempProjectDir ).removeRecursively();
123✔
1841

1842
  // add the local project if not there yet
1843
  if ( !mLocalProjects.projectFromMerginName( projectFullName ).isValid() )
123✔
1844
  {
1845
    QString projectNamespace, projectName;
61✔
1846
    extractProjectName( projectFullName, projectNamespace, projectName );
61✔
1847

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

1852
    mLocalProjects.addMerginProject( projectDir, projectNamespace, projectName );
61✔
1853
  }
61✔
1854

1855
  finishProjectSync( projectFullName, true );
123✔
1856
}
123✔
1857

1858

1859
void MerginApi::pushStartReplyFinished()
115✔
1860
{
1861
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
115✔
1862
  Q_ASSERT( r );
115✔
1863

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

1866
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
115✔
1867
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
115✔
1868
  Q_ASSERT( r == transaction.replyPushStart );
115✔
1869

1870
  if ( r->error() == QNetworkReply::NoError )
115✔
1871
  {
1872
    QByteArray data = r->readAll();
115✔
1873

1874
    transaction.replyPushStart->deleteLater();
115✔
1875
    transaction.replyPushStart = nullptr;
115✔
1876

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

1889
      if ( transaction.transactionUUID.isEmpty() )
111✔
1890
      {
1891
        CoreUtils::log( "push " + projectFullName, QStringLiteral( "Fail! Could not acquire transaction ID" ) );
×
1892
        finishProjectSync( projectFullName, false );
×
1893
      }
×
1894

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

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

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

1908
      transaction.projectMetadata = data;
4✔
1909
      transaction.version = MerginProjectMetadata::fromJson( data ).version;
4✔
1910

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

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

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

1927
    if ( showLimitReachedDialog )
×
1928
    {
1929
      const QList<MerginFile> files = transaction.pushQueue;
×
1930
      qreal uploadSize = 0;
×
1931
      for ( const MerginFile &f : files )
×
1932
      {
1933
        uploadSize += f.size;
×
1934
      }
1935
      emit storageLimitReached( uploadSize );
×
1936

1937
      // remove project if it was first time sync - migration
1938
      if ( transaction.isInitialPush )
×
1939
      {
1940
        QString projectNamespace, projectName;
×
1941
        extractProjectName( projectFullName, projectNamespace, projectName );
×
1942

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

1953
    finishProjectSync( projectFullName, false );
×
1954
  }
×
1955
}
115✔
1956

1957
void MerginApi::pushFileReplyFinished()
152✔
1958
{
1959
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
152✔
1960
  Q_ASSERT( r );
152✔
1961

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

1964
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
152✔
1965
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
152✔
1966
  Q_ASSERT( r == transaction.replyPushFile );
152✔
1967

1968
  QStringList params = ( r->url().toString().split( "/" ) );
152✔
1969
  QString transactionUUID = params.at( params.length() - 2 );
152✔
1970
  QString chunkID = params.at( params.length() - 1 );
152✔
1971
  Q_ASSERT( transactionUUID == transaction.transactionUUID );
152✔
1972

1973
  if ( r->error() == QNetworkReply::NoError )
152✔
1974
  {
1975
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Uploaded successfully: " ) + chunkID );
151✔
1976

1977
    transaction.replyPushFile->deleteLater();
151✔
1978
    transaction.replyPushFile = nullptr;
151✔
1979

1980
    MerginFile currentFile = transaction.pushQueue.first();
151✔
1981
    int chunkNo = currentFile.chunks.indexOf( chunkID );
151✔
1982
    if ( chunkNo < currentFile.chunks.size() - 1 )
151✔
1983
    {
1984
      pushFile( projectFullName, transactionUUID, currentFile, chunkNo + 1 );
2✔
1985
    }
2✔
1986
    else
1987
    {
1988
      transaction.transferedSize += currentFile.size;
149✔
1989

1990
      emit syncProjectStatusChanged( projectFullName, transaction.transferedSize / transaction.totalSize );
149✔
1991
      transaction.pushQueue.removeFirst();
149✔
1992

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

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

2012
    transaction.replyPushFile->deleteLater();
1✔
2013
    transaction.replyPushFile = nullptr;
1✔
2014

2015
    finishProjectSync( projectFullName, false );
1✔
2016
  }
1✔
2017
}
152✔
2018

2019
void MerginApi::pullInfoReplyFinished()
100✔
2020
{
2021
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
100✔
2022
  Q_ASSERT( r );
100✔
2023

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

2026
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
100✔
2027
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
100✔
2028
  Q_ASSERT( r == transaction.replyPullProjectInfo );
100✔
2029

2030
  if ( r->error() == QNetworkReply::NoError )
100✔
2031
  {
2032
    QByteArray data = r->readAll();
99✔
2033
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Downloaded project info." ) );
99✔
2034

2035
    transaction.replyPullProjectInfo->deleteLater();
99✔
2036
    transaction.replyPullProjectInfo = nullptr;
99✔
2037

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

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

2049
    transaction.replyPullProjectInfo->deleteLater();
1✔
2050
    transaction.replyPullProjectInfo = nullptr;
1✔
2051

2052
    finishProjectSync( projectFullName, false );
1✔
2053
  }
1✔
2054
}
100✔
2055

2056
void MerginApi::prepareProjectPull( const QString &projectFullName, const QByteArray &data )
125✔
2057
{
2058
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
125✔
2059
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
125✔
2060

2061
  MerginProjectMetadata serverProject = MerginProjectMetadata::fromJson( data );
125✔
2062

2063
  transaction.projectMetadata = data;
125✔
2064
  transaction.version = serverProject.version;
125✔
2065

2066
  LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName );
125✔
2067
  if ( projectInfo.isValid() )
125✔
2068
  {
2069
    transaction.projectDir = projectInfo.projectDir;
63✔
2070

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

2077
      return finishProjectSync( projectFullName, false );
1✔
2078
    }
2079
  }
62✔
2080
  else
2081
  {
2082
    QString projectNamespace;
62✔
2083
    QString projectName;
62✔
2084
    extractProjectName( projectFullName, projectNamespace, projectName );
62✔
2085

2086
    // remove any leftover temp files that could be created from previous unsuccessful download
2087
    removeProjectsTempFolder( projectNamespace, projectName );
62✔
2088

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

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

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

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

2104
  if ( transaction.configAllowed )
124✔
2105
  {
2106
    prepareDownloadConfig( projectFullName );
112✔
2107
  }
112✔
2108
  else
2109
  {
2110
    startProjectPull( projectFullName );
12✔
2111
  }
2112
}
125✔
2113

2114
void MerginApi::startProjectPull( const QString &projectFullName )
124✔
2115
{
2116
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
124✔
2117
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
124✔
2118

2119
  QList<MerginFile> localFiles = getLocalProjectFiles( transaction.projectDir + "/" );
124✔
2120
  MerginProjectMetadata serverProject = MerginProjectMetadata::fromJson( transaction.projectMetadata );
124✔
2121
  MerginProjectMetadata oldServerProject = MerginProjectMetadata::fromCachedJson( transaction.projectDir + "/" + sMetadataFile );
124✔
2122
  MerginConfig oldTransactionConfig = MerginConfig::fromFile( transaction.projectDir + "/" + sMerginConfigFile );
124✔
2123

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

2127
  transaction.diff = compareProjectFiles(
124✔
2128
                       oldServerProject.files,
124✔
2129
                       serverProject.files,
124✔
2130
                       localFiles,
2131
                       transaction.projectDir,
124✔
2132
                       transaction.configAllowed,
124✔
2133
                       transaction.config,
124✔
2134
                       oldTransactionConfig );
2135

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

2138
  for ( QString filePath : transaction.diff.remoteAdded )
317✔
2139
  {
2140
    MerginFile file = serverProject.fileInfo( filePath );
193✔
2141
    QList<DownloadQueueItem> items = itemsForFileChunks( file, transaction.version );
193✔
2142
    transaction.pullTasks << PullTask( PullTask::Copy, filePath, items );
193✔
2143
    transaction.gpkgSchemaChanged = true;
193✔
2144
  }
193✔
2145

2146
  for ( QString filePath : transaction.diff.remoteUpdated )
138✔
2147
  {
2148
    MerginFile file = serverProject.fileInfo( filePath );
14✔
2149

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

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

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

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

2192
  // schedule removed files to be deleted
2193
  for ( QString filePath : transaction.diff.remoteDeleted )
127✔
2194
  {
2195
    transaction.pullTasks << PullTask( PullTask::Delete, filePath, QList<DownloadQueueItem>() );
3✔
2196
  }
3✔
2197

2198
  // prepare the download queue
2199
  for ( const PullTask &item : transaction.pullTasks )
340✔
2200
  {
2201
    transaction.downloadQueue << item.data;
216✔
2202
  }
2203

2204
  qint64 totalSize = 0;
124✔
2205
  for ( const DownloadQueueItem &item : transaction.downloadQueue )
277✔
2206
  {
2207
    totalSize += item.size;
153✔
2208
  }
2209
  transaction.totalSize = totalSize;
124✔
2210

2211
  CoreUtils::log( "pull " + projectFullName, QStringLiteral( "%1 update tasks, %2 items to download (total size %3 bytes)" )
248✔
2212
                  .arg( transaction.pullTasks.count() )
124✔
2213
                  .arg( transaction.downloadQueue.count() )
124✔
2214
                  .arg( transaction.totalSize ) );
124✔
2215

2216
  emit pullFilesStarted();
124✔
2217
  downloadNextItem( projectFullName );
124✔
2218
}
124✔
2219

2220
void MerginApi::prepareDownloadConfig( const QString &projectFullName, bool downloaded )
151✔
2221
{
2222
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
151✔
2223
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
151✔
2224

2225
  MerginProjectMetadata newServerVersion = MerginProjectMetadata::fromJson( transaction.projectMetadata );
151✔
2226

2227
  const auto res = std::find_if( newServerVersion.files.begin(), newServerVersion.files.end(), []( const MerginFile & file )
680✔
2228
  {
2229
    return file.path == sMerginConfigFile;
529✔
2230
  } );
2231
  bool serverContainsConfig = res != newServerVersion.files.end();
151✔
2232

2233
  if ( serverContainsConfig )
151✔
2234
  {
2235
    if ( !downloaded )
78✔
2236
    {
2237
      // we should have server config but we do not have it yet
2238
      return requestServerConfig( projectFullName );
39✔
2239
    }
2240
  }
39✔
2241

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

2244
  const auto resOld = std::find_if( oldServerVersion.files.begin(), oldServerVersion.files.end(), []( const MerginFile & file )
317✔
2245
  {
2246
    return file.path == sMerginConfigFile;
205✔
2247
  } );
2248

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

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

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

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

2311
    // if it would be possible to add mergin-config locally, it needs to be checked here
2312
  }
2313

2314
  startProjectPull( projectFullName );
112✔
2315
}
151✔
2316

2317
void MerginApi::requestServerConfig( const QString &projectFullName )
39✔
2318
{
2319
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
39✔
2320
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
39✔
2321

2322
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/raw/" ) + projectFullName );
39✔
2323
  QUrlQuery query;
39✔
2324

2325
  query.addQueryItem( "file", sMerginConfigFile.toUtf8().toPercentEncoding() );
39✔
2326
  query.addQueryItem( "version", QStringLiteral( "v%1" ).arg( transaction.version ) );
39✔
2327
  url.setQuery( query );
39✔
2328

2329
  QNetworkRequest request = getDefaultRequest();
39✔
2330
  request.setUrl( url );
39✔
2331
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
39✔
2332

2333
  Q_ASSERT( !transaction.replyPullItem );
39✔
2334
  transaction.replyPullItem = mManager.get( request );
39✔
2335
  connect( transaction.replyPullItem, &QNetworkReply::finished, this, &MerginApi::cacheServerConfig );
39✔
2336

2337
  CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Requesting mergin config: " ) + url.toString() );
39✔
2338
}
39✔
2339

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

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

2364

2365
static MerginFile findFile( const QString &filePath, const QList<MerginFile> &files )
155✔
2366
{
2367
  for ( const MerginFile &merginFile : files )
422✔
2368
  {
2369
    if ( merginFile.path == filePath )
422✔
2370
      return merginFile;
155✔
2371
  }
2372
  CoreUtils::log( QStringLiteral( "MerginFile" ), QStringLiteral( "requested findFile() for non-existant file: %1" ).arg( filePath ) );
×
2373
  return MerginFile();
×
2374
}
155✔
2375

2376

2377
void MerginApi::pushInfoReplyFinished()
143✔
2378
{
2379
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
143✔
2380
  Q_ASSERT( r );
143✔
2381

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

2384
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
143✔
2385
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
143✔
2386
  Q_ASSERT( r == transaction.replyPushProjectInfo );
143✔
2387

2388
  if ( r->error() == QNetworkReply::NoError )
143✔
2389
  {
2390
    QString url = r->url().toString();
142✔
2391
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Downloaded project info." ) );
142✔
2392
    QByteArray data = r->readAll();
142✔
2393

2394
    transaction.replyPushProjectInfo->deleteLater();
142✔
2395
    transaction.replyPushProjectInfo = nullptr;
142✔
2396

2397
    LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName );
142✔
2398
    transaction.projectDir = projectInfo.projectDir;
142✔
2399
    Q_ASSERT( !transaction.projectDir.isEmpty() );
142✔
2400

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

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

2415
    QList<MerginFile> localFiles = getLocalProjectFiles( transaction.projectDir + "/" );
116✔
2416
    MerginProjectMetadata oldServerProject = MerginProjectMetadata::fromCachedJson( transaction.projectDir + "/" + sMetadataFile );
116✔
2417

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

2424
    transaction.diff = compareProjectFiles(
116✔
2425
                         oldServerProject.files,
116✔
2426
                         serverProject.files,
116✔
2427
                         localFiles,
2428
                         transaction.projectDir,
116✔
2429
                         transaction.configAllowed,
116✔
2430
                         transaction.config
116✔
2431
                       );
2432

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

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

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

2447
    for ( QString filePath : transaction.diff.localUpdated )
135✔
2448
    {
2449
      MerginFile merginFile = findFile( filePath, localFiles );
19✔
2450
      merginFile.chunks = generateChunkIdsForSize( merginFile.size );
19✔
2451

2452
      if ( MerginApi::isFileDiffable( filePath ) )
19✔
2453
      {
2454
        // try to create a diff
2455
        QString diffName;
12✔
2456
        int geodiffRes = GeodiffUtils::createChangeset( transaction.projectDir, filePath, diffName );
12✔
2457
        QString diffPath = transaction.projectDir + "/.mergin/" + diffName;
12✔
2458
        QString basePath = transaction.projectDir + "/.mergin/" + filePath;
12✔
2459

2460
        if ( geodiffRes == GEODIFF_SUCCESS )
12✔
2461
        {
2462
          QByteArray checksumDiff = getChecksum( diffPath );
12✔
2463

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

2468
          merginFile.diffName = diffName;
12✔
2469
          merginFile.diffChecksum = QString::fromLatin1( checksumDiff.data(), checksumDiff.size() );
12✔
2470
          merginFile.diffSize = QFileInfo( diffPath ).size();
12✔
2471
          merginFile.chunks = generateChunkIdsForSize( merginFile.diffSize );
12✔
2472
          merginFile.diffBaseChecksum = QString::fromLatin1( checksumBase.data(), checksumBase.size() );
12✔
2473

2474
          diffFiles.append( merginFile );
12✔
2475

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

2485
      updatedMerginFiles.append( merginFile );
19✔
2486
    }
19✔
2487

2488
    for ( QString filePath : transaction.diff.localDeleted )
120✔
2489
    {
2490
      MerginFile merginFile = findFile( filePath, serverProject.files );
4✔
2491
      deletedMerginFiles.append( merginFile );
4✔
2492
    }
4✔
2493

2494
    if ( addedMerginFiles.isEmpty() && updatedMerginFiles.isEmpty() && deletedMerginFiles.isEmpty() )
116✔
2495
    {
2496
      // if nothing has changed, there is no point to even start upload transaction
2497
      transaction.projectMetadata = data;
1✔
2498
      transaction.version = MerginProjectMetadata::fromJson( data ).version;
1✔
2499

2500
      finishProjectSync( projectFullName, true );
1✔
2501
      return;
1✔
2502
    }
2503

2504
    QJsonArray added = prepareUploadChangesJSON( addedMerginFiles );
115✔
2505
    filesToUpload.append( addedMerginFiles );
115✔
2506

2507
    QJsonArray modified = prepareUploadChangesJSON( updatedMerginFiles );
115✔
2508
    filesToUpload.append( updatedMerginFiles );
115✔
2509

2510
    QJsonArray removed = prepareUploadChangesJSON( deletedMerginFiles );
115✔
2511
    // removed not in filesToUpload
2512

2513
    QJsonObject changes;
115✔
2514
    changes.insert( "added", added );
115✔
2515
    changes.insert( "removed", removed );
115✔
2516
    changes.insert( "updated", modified );
115✔
2517
    changes.insert( "renamed", QJsonArray() );
115✔
2518

2519
    qint64 totalSize = 0;
115✔
2520
    for ( MerginFile file : filesToUpload )
266✔
2521
    {
2522
      if ( !file.diffName.isEmpty() )
151✔
2523
        totalSize += file.diffSize;
12✔
2524
      else
2525
        totalSize += file.size;
139✔
2526
    }
151✔
2527

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

2531
    transaction.totalSize = totalSize;
115✔
2532
    transaction.pushQueue = filesToUpload;
115✔
2533
    transaction.pushDiffFiles = diffFiles;
115✔
2534

2535
    QJsonObject json;
115✔
2536
    json.insert( QStringLiteral( "changes" ), changes );
115✔
2537
    json.insert( QStringLiteral( "version" ), QString( "v%1" ).arg( serverProject.version ) );
115✔
2538
    QJsonDocument jsonDoc;
115✔
2539
    jsonDoc.setObject( json );
115✔
2540

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

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

2552
    transaction.replyPushProjectInfo->deleteLater();
1✔
2553
    transaction.replyPushProjectInfo = nullptr;
1✔
2554

2555
    finishProjectSync( projectFullName, false );
1✔
2556
  }
1✔
2557
}
143✔
2558

2559
void MerginApi::pushFinishReplyFinished()
110✔
2560
{
2561
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
110✔
2562
  Q_ASSERT( r );
110✔
2563

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

2566
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
110✔
2567
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
110✔
2568
  Q_ASSERT( r == transaction.replyPushFinish );
110✔
2569

2570
  if ( r->error() == QNetworkReply::NoError )
110✔
2571
  {
2572
    Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
110✔
2573
    QByteArray data = r->readAll();
110✔
2574
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Transaction finish accepted" ) );
110✔
2575

2576
    transaction.replyPushFinish->deleteLater();
110✔
2577
    transaction.replyPushFinish = nullptr;
110✔
2578

2579
    transaction.projectMetadata = data;
110✔
2580
    transaction.version = MerginProjectMetadata::fromJson( data ).version;
110✔
2581

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

2590
        QString sourcePath = transaction.projectDir + "/" + filePath;
11✔
2591
        if ( !QFile::copy( sourcePath, basefile ) )
11✔
2592
        {
2593
          CoreUtils::log( "push " + projectFullName, "failed to copy new basefile for: " + filePath );
×
2594
        }
×
2595
      }
11✔
2596
    }
130✔
2597

2598
    // clean up diff-related files
2599
    const auto diffFiles = transaction.pushDiffFiles;
110✔
2600
    for ( const MerginFile &merginFile : diffFiles )
122✔
2601
    {
2602
      QString diffPath = transaction.projectDir + "/.mergin/" + merginFile.diffName;
12✔
2603

2604
      // update basefile (unmodified file that should be equivalent to the server)
2605
      QString basePath = transaction.projectDir + "/.mergin/" + merginFile.path;
12✔
2606
      bool res = GeodiffUtils::applyChangeset( basePath, diffPath );
12✔
2607
      if ( res )
12✔
2608
      {
2609
        CoreUtils::log( "push " + projectFullName, QString( "Applied %1 to base file of %2" ).arg( merginFile.diffName, merginFile.path ) );
12✔
2610
      }
12✔
2611
      else
2612
      {
2613
        CoreUtils::log( "push " + projectFullName, QString( "Failed to apply changeset %1 to basefile %2 - error %3" ).arg( diffPath ).arg( basePath ).arg( res ) );
×
2614
      }
2615

2616
      // remove temporary diff files
2617
      if ( !QFile::remove( diffPath ) )
12✔
2618
        CoreUtils::log( "push " + projectFullName, "Failed to remove diff: " + diffPath );
×
2619
    }
12✔
2620

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

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

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

2641
    transaction.replyPushFinish->deleteLater();
×
2642
    transaction.replyPushFinish = nullptr;
×
2643

2644
    finishProjectSync( projectFullName, false );
×
2645
  }
×
2646
}
110✔
2647

2648
void MerginApi::pushCancelReplyFinished()
1✔
2649
{
2650
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
1✔
2651
  Q_ASSERT( r );
1✔
2652

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

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

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

2668
  r->deleteLater();
1✔
2669
}
1✔
2670

2671
void MerginApi::getUserInfoFinished()
19✔
2672
{
2673
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
19✔
2674
  Q_ASSERT( r );
19✔
2675

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

2699
  emit userInfoReplyFinished();
19✔
2700

2701
  r->deleteLater();
19✔
2702
}
19✔
2703

2704
void MerginApi::getWorkspaceInfoReplyFinished()
23✔
2705
{
2706
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
23✔
2707
  Q_ASSERT( r );
23✔
2708

2709
  if ( r->error() == QNetworkReply::NoError )
23✔
2710
  {
2711
    CoreUtils::log( "workspace info", QStringLiteral( "Success" ) );
23✔
2712
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
23✔
2713
    if ( doc.isObject() )
23✔
2714
    {
2715
      QJsonObject docObj = doc.object();
23✔
2716
      mWorkspaceInfo->setFromJson( docObj );
23✔
2717

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

2730
  r->deleteLater();
23✔
2731
}
23✔
2732

2733
ProjectDiff MerginApi::compareProjectFiles(
279✔
2734
  const QList<MerginFile> &oldServerFiles,
2735
  const QList<MerginFile> &newServerFiles,
2736
  const QList<MerginFile> &localFiles,
2737
  const QString &projectDir,
2738
  bool allowConfig,
2739
  const MerginConfig &config,
2740
  const MerginConfig &lastSyncConfig
2741
)
2742
{
2743
  ProjectDiff diff;
279✔
2744
  QHash<QString, MerginFile> oldServerFilesMap, newServerFilesMap;
279✔
2745

2746
  for ( MerginFile file : newServerFiles )
1,477✔
2747
  {
2748
    newServerFilesMap.insert( file.path, file );
1,198✔
2749
  }
1,198✔
2750
  for ( MerginFile file : oldServerFiles )
1,271✔
2751
  {
2752
    oldServerFilesMap.insert( file.path, file );
992✔
2753
  }
992✔
2754

2755
  for ( MerginFile localFile : localFiles )
1,268✔
2756
  {
2757
    QString filePath = localFile.path;
989✔
2758
    bool hasOldServer = oldServerFilesMap.contains( localFile.path );
989✔
2759
    bool hasNewServer = newServerFilesMap.contains( localFile.path );
989✔
2760
    QString chkOld = oldServerFilesMap.value( localFile.path ).checksum;
989✔
2761
    QString chkNew = newServerFilesMap.value( localFile.path ).checksum;
989✔
2762
    QString chkLocal = localFile.checksum;
989✔
2763

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

2850
    if ( hasOldServer )
989✔
2851
      oldServerFilesMap.remove( filePath );
813✔
2852
    if ( hasNewServer )
989✔
2853
      newServerFilesMap.remove( filePath );
810✔
2854
  }
989✔
2855

2856
  // go through files listed on the server, but not available locally
2857
  for ( MerginFile file : newServerFilesMap )
667✔
2858
  {
2859
    bool hasOldServer = oldServerFilesMap.contains( file.path );
388✔
2860

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

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

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

2907
    if ( hasOldServer )
179✔
2908
      oldServerFilesMap.remove( file.path );
5✔
2909
  }
388✔
2910

2911
  for ( MerginFile file : oldServerFilesMap )
453✔
2912
  {
2913
    // R-D/L-D
2914
    // TODO: need to do anything?
2915
  }
174✔
2916

2917
  return diff;
279✔
2918
}
279✔
2919

2920
MerginProject MerginApi::parseProjectMetadata( const QJsonObject &proj )
286✔
2921
{
2922
  MerginProject project;
286✔
2923

2924
  if ( proj.isEmpty() )
286✔
2925
  {
2926
    return project;
×
2927
  }
2928

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

2936
  project.projectName = proj.value( QStringLiteral( "name" ) ).toString();
286✔
2937
  project.projectNamespace = proj.value( QStringLiteral( "namespace" ) ).toString();
286✔
2938

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

2950
  QDateTime updated = QDateTime::fromString( proj.value( QStringLiteral( "updated" ) ).toString(), Qt::ISODateWithMs ).toUTC();
286✔
2951
  if ( !updated.isValid() )
286✔
2952
  {
2953
    project.serverUpdated = QDateTime::fromString( proj.value( QStringLiteral( "created" ) ).toString(), Qt::ISODateWithMs ).toUTC();
×
2954
  }
×
2955
  else
2956
  {
2957
    project.serverUpdated = updated;
286✔
2958
  }
2959
  return project;
286✔
2960
}
286✔
2961

2962

2963
MerginProjectsList MerginApi::parseProjectsFromJson( const QJsonDocument &doc )
29✔
2964
{
2965
  if ( !doc.isObject() )
29✔
2966
    return MerginProjectsList();
×
2967

2968
  QJsonObject object = doc.object();
29✔
2969
  MerginProjectsList result;
29✔
2970

2971
  if ( object.contains( "projects" ) && object.value( "projects" ).isArray() ) // listProjects API
51✔
2972
  {
2973
    QJsonArray vArray = object.value( "projects" ).toArray();
22✔
2974

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

2996
void MerginApi::refreshAuthToken()
7✔
2997
{
2998
  if ( !mUserAuth->hasAuthData() ||
7✔
2999
       mUserAuth->authToken().isEmpty() )
7✔
3000
  {
3001
    CoreUtils::log( QStringLiteral( "Auth" ), QStringLiteral( "Can not refresh token, missing credentials" ) );
×
3002
    return;
×
3003
  }
3004

3005
  if ( mUserAuth->tokenExpiration() < QDateTime::currentDateTimeUtc() )
7✔
3006
  {
3007
    CoreUtils::log( QStringLiteral( "Auth" ), QStringLiteral( "Token has expired, requesting new one" ) );
×
3008
    authorize( mUserAuth->username(), mUserAuth->password() );
×
3009
    mAuthLoopEvent.exec();
×
3010
  }
×
3011
}
7✔
3012

3013
QStringList MerginApi::generateChunkIdsForSize( qint64 fileSize )
163✔
3014
{
3015
  qreal rawNoOfChunks = qreal( fileSize ) / UPLOAD_CHUNK_SIZE;
163✔
3016
  int noOfChunks = qCeil( rawNoOfChunks );
163✔
3017

3018
  // edge case when file is empty, filesize equals zero
3019
  // manually set one chunk so that file will be synced
3020
  if ( fileSize <= 0 )
163✔
3021
    noOfChunks = 1;
45✔
3022

3023
  QStringList chunks;
163✔
3024
  for ( int i = 0; i < noOfChunks; i++ )
328✔
3025
  {
3026
    QString chunkID = CoreUtils::uuidWithoutBraces( QUuid::createUuid() );
165✔
3027
    chunks.append( chunkID );
165✔
3028
  }
165✔
3029
  return chunks;
163✔
3030
}
163✔
3031

3032
QJsonArray MerginApi::prepareUploadChangesJSON( const QList<MerginFile> &files )
345✔
3033
{
3034
  QJsonArray jsonArray;
345✔
3035

3036
  for ( MerginFile file : files )
500✔
3037
  {
3038
    QJsonObject fileObject;
155✔
3039
    fileObject.insert( "path", file.path );
155✔
3040

3041
    fileObject.insert( "size", file.size );
155✔
3042
    fileObject.insert( "mtime", file.mtime.toString( Qt::ISODateWithMs ) );
155✔
3043

3044
    if ( !file.diffName.isEmpty() )
155✔
3045
    {
3046
      // doing diff-based upload
3047
      QJsonObject diffObject;
12✔
3048
      diffObject.insert( "path", file.diffName );
12✔
3049
      diffObject.insert( "checksum", file.diffChecksum );
12✔
3050
      diffObject.insert( "size", file.diffSize );
12✔
3051

3052
      fileObject.insert( "diff", diffObject );
12✔
3053
      fileObject.insert( "checksum", file.diffBaseChecksum );
12✔
3054
    }
12✔
3055
    else
3056
    {
3057
      fileObject.insert( "checksum", file.checksum );
143✔
3058
    }
3059

3060
    QJsonArray chunksJson;
155✔
3061
    for ( QString id : file.chunks )
308✔
3062
    {
3063
      chunksJson.append( id );
153✔
3064
    }
153✔
3065
    fileObject.insert( "chunks", chunksJson );
155✔
3066
    jsonArray.append( fileObject );
155✔
3067
  }
155✔
3068
  return jsonArray;
345✔
3069
}
345✔
3070

3071
void MerginApi::finishProjectSync( const QString &projectFullName, bool syncSuccessful )
243✔
3072
{
3073
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
243✔
3074
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
243✔
3075

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

3078
  if ( syncSuccessful )
243✔
3079
  {
3080
    // update the local metadata file
3081
    writeData( transaction.projectMetadata, transaction.projectDir + "/" + MerginApi::sMetadataFile );
238✔
3082

3083
    // update info of local projects
3084
    mLocalProjects.updateLocalVersion( transaction.projectDir, transaction.version );
238✔
3085

3086
    CoreUtils::log( "sync " + projectFullName, QStringLiteral( "### Finished ###  New project version: %1\n" ).arg( transaction.version ) );
238✔
3087
  }
238✔
3088
  else
3089
  {
3090
    CoreUtils::log( "sync " + projectFullName, QStringLiteral( "### FAILED ###\n" ) );
5✔
3091
  }
3092

3093
  bool pullBeforePush = transaction.pullBeforePush;
243✔
3094
  QString projectDir = transaction.projectDir;  // keep it before the transaction gets removed
243✔
3095
  ProjectDiff diff = transaction.diff;
243✔
3096
  int newVersion = syncSuccessful ? transaction.version : -1;
243✔
3097
  mTransactionalStatus.remove( projectFullName );
243✔
3098

3099
  if ( transaction.gpkgSchemaChanged )
243✔
3100
  {
3101
    emit projectReloadNeededAfterSync( projectFullName );
96✔
3102
  }
96✔
3103

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

3116
    if ( syncSuccessful )
217✔
3117
    {
3118
      emit projectDataChanged( projectFullName );
212✔
3119
    }
212✔
3120
  }
3121
}
243✔
3122

3123
bool MerginApi::writeData( const QByteArray &data, const QString &path )
238✔
3124
{
3125
  QFile file( path );
238✔
3126
  createPathIfNotExists( path );
238✔
3127
  if ( !file.open( QIODevice::WriteOnly ) )
238✔
3128
  {
3129
    return false;
×
3130
  }
3131

3132
  file.write( data );
238✔
3133
  file.close();
238✔
3134

3135
  return true;
238✔
3136
}
238✔
3137

3138

3139
void MerginApi::createPathIfNotExists( const QString &filePath )
706✔
3140
{
3141
  QDir dir;
706✔
3142
  if ( !dir.exists( mDataDir ) )
706✔
3143
    dir.mkpath( mDataDir );
×
3144

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

3155
bool MerginApi::isInIgnore( const QFileInfo &info )
2,351✔
3156
{
3157
  return sIgnoreExtensions.contains( info.suffix() ) || sIgnoreFiles.contains( info.fileName() ) || info.filePath().contains( sMetadataFolder + "/" );
2,351✔
3158
}
×
3159

3160
bool MerginApi::excludeFromSync( const QString &filePath, const MerginConfig &config )
377✔
3161
{
3162
  if ( config.isValid && config.selectiveSyncEnabled )
377✔
3163
  {
3164
    QFileInfo info( filePath );
247✔
3165

3166
    bool isExcludedFormat = sIgnoreImageExtensions.contains( info.suffix().toLower() );
247✔
3167

3168
    if ( !isExcludedFormat )
247✔
3169
      return false;
20✔
3170

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

3183
QByteArray MerginApi::getChecksum( const QString &filePath )
1,005✔
3184
{
3185
  QFile f( filePath );
1,005✔
3186
  if ( f.open( QFile::ReadOnly ) )
1,005✔
3187
  {
3188
    QCryptographicHash hash( QCryptographicHash::Sha1 );
1,005✔
3189
    QByteArray chunk = f.read( CHUNK_SIZE );
1,005✔
3190
    while ( !chunk.isEmpty() )
2,586✔
3191
    {
3192
      hash.addData( chunk );
1,581✔
3193
      chunk = f.read( CHUNK_SIZE );
1,581✔
3194
    }
3195
    f.close();
1,005✔
3196
    return hash.result().toHex();
1,005✔
3197
  }
1,005✔
3198

3199
  return QByteArray();
×
3200
}
1,005✔
3201

3202
QSet<QString> MerginApi::listFiles( const QString &path )
279✔
3203
{
3204
  QSet<QString> files;
279✔
3205
  QDirIterator it( path, QStringList() << QStringLiteral( "*" ), QDir::Files, QDirIterator::Subdirectories );
279✔
3206
  while ( it.hasNext() )
1,268✔
3207
  {
3208
    it.next();
989✔
3209
    if ( !isInIgnore( it.fileInfo() ) )
989✔
3210
    {
3211
      files << it.filePath().replace( path, "" );
989✔
3212
    }
989✔
3213
  }
3214
  return files;
279✔
3215
}
279✔
3216

3217
void MerginApi::deleteAccount()
1✔
3218
{
3219
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
1✔
3220
  {
3221
    return;
×
3222
  }
3223

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

3232
void MerginApi::deleteAccountFinished()
1✔
3233
{
3234
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
1✔
3235
  Q_ASSERT( r );
1✔
3236

3237
  if ( r->error() == QNetworkReply::NoError )
1✔
3238
  {
3239
    CoreUtils::log( "delete account " + mUserAuth->username(), QStringLiteral( "Success" ) );
1✔
3240

3241
    // remove all local projects from the device
3242
    LocalProjectsList projects = mLocalProjects.projects();
1✔
3243
    for ( const LocalProject &info : projects )
36✔
3244
    {
3245
      mLocalProjects.removeLocalProject( info.id() );
35✔
3246
    }
3247

3248
    clearAuth();
1✔
3249

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

3266
    emit accountDeleted( false );
×
3267
  }
×
3268

3269
  r->deleteLater();
1✔
3270
}
1✔
3271

3272
void MerginApi::getServerConfig()
24✔
3273
{
3274
  QNetworkRequest request = getDefaultRequest();
24✔
3275
  QString urlString = mApiRoot + QStringLiteral( "/config" );
24✔
3276
  QUrl url( urlString );
24✔
3277
  request.setUrl( url );
24✔
3278

3279
  QNetworkReply *reply = mManager.get( request );
24✔
3280

3281
  connect( reply, &QNetworkReply::finished, this, &MerginApi::getServerConfigReplyFinished );
24✔
3282
  CoreUtils::log( "Config", QStringLiteral( "Requesting server configuration: " ) + url.toString() );
24✔
3283
}
24✔
3284

3285
void MerginApi::getServerConfigReplyFinished()
11✔
3286
{
3287
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
11✔
3288
  Q_ASSERT( r );
11✔
3289

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

3327
  r->deleteLater();
11✔
3328
}
11✔
3329

3330
MerginServerType::ServerType MerginApi::serverType() const
13✔
3331
{
3332
  return mServerType;
13✔
3333
}
3334

3335
void MerginApi::setServerType( const MerginServerType::ServerType &serverType )
16✔
3336
{
3337
  if ( mServerType != serverType )
16✔
3338
  {
3339
    if ( mServerType == MerginServerType::OLD && serverType == MerginServerType::SAAS )
6✔
3340
    {
3341
      emit serverWasUpgraded();
2✔
3342
    }
2✔
3343

3344
    mServerType = serverType;
6✔
3345
    QSettings settings;
6✔
3346
    settings.beginGroup( QStringLiteral( "Input/" ) );
6✔
3347
    settings.setValue( QStringLiteral( "serverType" ), mServerType );
6✔
3348
    settings.endGroup();
6✔
3349
    emit serverTypeChanged();
6✔
3350
    emit apiSupportsWorkspacesChanged();
6✔
3351
  }
6✔
3352
}
16✔
3353

3354
void MerginApi::listWorkspaces()
×
3355
{
3356
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
×
3357
  {
3358
    emit listWorkspacesFailed();
×
3359
    return;
×
3360
  }
3361

3362
  QUrl url( mApiRoot + QStringLiteral( "/v1/workspaces" ) );
×
3363
  QNetworkRequest request = getDefaultRequest( mUserAuth->hasAuthData() );
×
3364
  request.setUrl( url );
×
3365

3366
  QNetworkReply *reply = mManager.get( request );
×
3367
  CoreUtils::log( "list workspaces", QStringLiteral( "Requesting: " ) + url.toString() );
×
3368
  connect( reply, &QNetworkReply::finished, this, &MerginApi::listWorkspacesReplyFinished );
×
3369
}
×
3370

3371
void MerginApi::listWorkspacesReplyFinished()
×
3372
{
3373
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
×
3374
  Q_ASSERT( r );
×
3375

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

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

3407
  r->deleteLater();
×
3408
}
×
3409

3410
void MerginApi::listInvitations()
×
3411
{
3412
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
×
3413
  {
3414
    emit listInvitationsFailed();
×
3415
    return;
×
3416
  }
3417

3418
  QUrl url( mApiRoot + QStringLiteral( "/v1/workspace/invitations" ) );
×
3419
  QNetworkRequest request = getDefaultRequest( mUserAuth->hasAuthData() );
×
3420
  request.setUrl( url );
×
3421

3422
  QNetworkReply *reply = mManager.get( request );
×
3423
  CoreUtils::log( "list invitations", QStringLiteral( "Requesting: " ) + url.toString() );
×
3424
  connect( reply, &QNetworkReply::finished, this, &MerginApi::listInvitationsReplyFinished );
×
3425
}
×
3426

3427
void MerginApi::listInvitationsReplyFinished()
×
3428
{
3429
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
×
3430
  Q_ASSERT( r );
×
3431

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

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

3462
  r->deleteLater();
×
3463
}
×
3464

3465
void MerginApi::processInvitation( const QString &uuid, bool accept )
×
3466
{
3467
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
×
3468
  {
3469
    emit processInvitationFailed();
×
3470
    return;
×
3471
  }
3472

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

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

3490
void MerginApi::processInvitationReplyFinished()
×
3491
{
3492
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
×
3493
  Q_ASSERT( r );
×
3494

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

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

3510
  emit processInvitationFinished( accept );
×
3511

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

3515
bool MerginApi::createWorkspace( const QString &workspaceName )
2✔
3516
{
3517
  if ( !validateAuth() )
2✔
3518
  {
3519
    emit missingAuthorizationError( workspaceName );
×
3520
    return false;
×
3521
  }
3522

3523
  if ( mApiVersionStatus != MerginApiStatus::OK )
2✔
3524
  {
3525
    return false;
×
3526
  }
3527

3528
  if ( !CoreUtils::isValidName( workspaceName ) )
2✔
3529
  {
3530
    emit notify( tr( "Workspace name contains invalid characters" ) );
×
3531
    return false;
×
3532
  }
3533

3534
  QNetworkRequest request = getDefaultRequest();
2✔
3535
  QUrl url( mApiRoot + QString( "/v1/workspace" ) );
2✔
3536
  request.setUrl( url );
2✔
3537
  request.setRawHeader( "Content-Type", "application/json" );
2✔
3538
  request.setRawHeader( "Accept", "application/json" );
2✔
3539
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrWorkspaceName ), workspaceName );
2✔
3540

3541
  QJsonDocument jsonDoc;
2✔
3542
  QJsonObject jsonObject;
2✔
3543
  jsonObject.insert( QStringLiteral( "name" ), workspaceName );
2✔
3544
  jsonDoc.setObject( jsonObject );
2✔
3545
  QByteArray json = jsonDoc.toJson( QJsonDocument::Compact );
2✔
3546

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

3551
  return true;
2✔
3552
}
2✔
3553

3554
void MerginApi::signOut()
×
3555
{
3556
  clearAuth();
×
3557
}
×
3558

3559
void MerginApi::refreshUserData()
×
3560
{
3561
  getUserInfo();
×
3562

3563
  if ( apiSupportsWorkspaces() )
×
3564
  {
3565
    getWorkspaceInfo();
×
3566
    // getServiceInfo is called automatically when workspace info finishes
3567
  }
×
3568
  else if ( mServerType == MerginServerType::OLD )
×
3569
  {
3570
    getServiceInfo();
×
3571
  }
×
3572
}
×
3573

3574
void MerginApi::createWorkspaceReplyFinished()
2✔
3575
{
3576
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
2✔
3577
  Q_ASSERT( r );
2✔
3578

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

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

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

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

3607
bool MerginApi::apiSupportsWorkspaces()
×
3608
{
3609
  if ( mServerType == MerginServerType::SAAS || mServerType == MerginServerType::EE )
×
3610
  {
3611
    return true;
×
3612
  }
3613
  else
3614
  {
3615
    return false;
×
3616
  }
3617
}
×
3618

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

© 2026 Coveralls, Inc