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

MerginMaps / input / 6622756759

23 Oct 2023 11:17AM UTC coverage: 62.291% (-0.05%) from 62.34%
6622756759

push

github

tomasMizera
update strings

7688 of 12342 relevant lines covered (62.29%)

105.25 hits per line

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

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

10
#include "merginapi.h"
11

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

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

29
#include <geodiff.h>
30

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

41

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

63
  qRegisterMetaType<Transactions>();
23✔
64

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

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

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

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

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

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

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

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

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

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

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

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

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

155
  QUrlQuery query;
22✔
156

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

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

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

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

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

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

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

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

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

204
  return requestId;
22✔
205
}
22✔
206

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

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

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

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

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

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

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

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

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

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

253
  return requestId;
7✔
254
}
7✔
255

256

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

262
  if ( transaction.downloadQueue.isEmpty() )
275✔
263
  {
264
    // there's nothing to download so just finalize the pull
265
    finalizeProjectPull( projectFullName );
123✔
266
    return;
123✔
267
  }
268

269
  DownloadQueueItem item = transaction.downloadQueue.takeFirst();
152✔
270

271
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/raw/" ) + projectFullName );
304✔
272
  QUrlQuery query;
152✔
273
  // Handles special chars in a filePath (e.g prevents to convert "+" sign into a space)
274
  query.addQueryItem( "file", item.filePath.toUtf8().toPercentEncoding() );
152✔
275
  query.addQueryItem( "version", QStringLiteral( "v%1" ).arg( item.version ) );
152✔
276
  if ( item.downloadDiff )
152✔
277
    query.addQueryItem( "diff", "true" );
10✔
278
  url.setQuery( query );
152✔
279

280
  QNetworkRequest request = getDefaultRequest();
152✔
281
  request.setUrl( url );
152✔
282
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
152✔
283
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrTempFileName ), item.tempFileName );
152✔
284

285
  QString range;
152✔
286
  if ( item.rangeFrom != -1 && item.rangeTo != -1 )
152✔
287
  {
288
    range = QStringLiteral( "bytes=%1-%2" ).arg( item.rangeFrom ).arg( item.rangeTo );
142✔
289
    request.setRawHeader( "Range", range.toUtf8() );
142✔
290
  }
291

292
  Q_ASSERT( !transaction.replyPullItem );
152✔
293
  transaction.replyPullItem = mManager.get( request );
152✔
294
  connect( transaction.replyPullItem, &QNetworkReply::finished, this, &MerginApi::downloadItemReplyFinished );
152✔
295

296
  CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Requesting item: " ) + url.toString() +
304✔
297
                  ( !range.isEmpty() ? " Range: " + range : QString() ) );
304✔
298
}
152✔
299

300
void MerginApi::removeProjectsTempFolder( const QString &projectNamespace, const QString &projectName )
62✔
301
{
302
  if ( projectNamespace.isEmpty() || projectName.isEmpty() )
62✔
303
    return; // otherwise we could remove enitre users temp or entire .temp
×
304

305
  QString path = getTempProjectDir( getFullProjectName( projectNamespace, projectName ) );
124✔
306
  QDir( path ).removeRecursively();
62✔
307
}
62✔
308

309
QNetworkRequest MerginApi::getDefaultRequest( bool withAuth )
1,078✔
310
{
311
  QNetworkRequest request;
1,078✔
312
  QString info = CoreUtils::appInfo();
1,078✔
313
  request.setRawHeader( "User-Agent", QByteArray( info.toUtf8() ) );
1,078✔
314
  if ( withAuth )
1,078✔
315
    request.setRawHeader( "Authorization", QByteArray( "Bearer " + mUserAuth->authToken() ) );
1,038✔
316

317
  return request;
2,156✔
318
}
1,078✔
319

320
bool MerginApi::projectFileHasBeenUpdated( const ProjectDiff &diff )
147✔
321
{
322
  for ( QString filePath : diff.remoteAdded )
149✔
323
  {
324
    if ( CoreUtils::hasProjectFileExtension( filePath ) )
2✔
325
      return true;
×
326
  }
2✔
327

328
  for ( QString filePath : diff.remoteUpdated )
153✔
329
  {
330
    if ( CoreUtils::hasProjectFileExtension( filePath ) )
6✔
331
      return true;
×
332
  }
6✔
333

334
  return false;
147✔
335
}
336

337
bool MerginApi::supportsSelectiveSync() const
×
338
{
339
  return mSupportsSelectiveSync;
×
340
}
341

342
void MerginApi::setSupportsSelectiveSync( bool supportsSelectiveSync )
4✔
343
{
344
  mSupportsSelectiveSync = supportsSelectiveSync;
4✔
345
}
4✔
346

347
bool MerginApi::apiSupportsSubscriptions() const
31✔
348
{
349
  return mApiSupportsSubscriptions;
31✔
350
}
351

352
void MerginApi::setApiSupportsSubscriptions( bool apiSupportsSubscriptions )
11✔
353
{
354
  if ( mApiSupportsSubscriptions != apiSupportsSubscriptions )
11✔
355
  {
356
    mApiSupportsSubscriptions = apiSupportsSubscriptions;
10✔
357
    emit apiSupportsSubscriptionsChanged();
10✔
358
  }
359
}
11✔
360

361
#if !defined(USE_MERGIN_DUMMY_API_KEY)
362
#include "merginsecrets.cpp"
363
#endif
364

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

377
void MerginApi::downloadItemReplyFinished()
152✔
378
{
379
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
152✔
380
  Q_ASSERT( r );
152✔
381

382
  QString projectFullName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ) ).toString();
304✔
383
  QString tempFileName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrTempFileName ) ).toString();
304✔
384

385
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
152✔
386
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
152✔
387
  Q_ASSERT( r == transaction.replyPullItem );
152✔
388

389
  if ( r->error() == QNetworkReply::NoError )
152✔
390
  {
391
    QByteArray data = r->readAll();
151✔
392

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

395
    QString tempFolder = getTempProjectDir( projectFullName );
151✔
396
    QString tempFilePath = tempFolder + "/" + tempFileName;
151✔
397
    createPathIfNotExists( tempFilePath );
151✔
398

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

411
    transaction.transferedSize += data.size();
151✔
412
    emit syncProjectStatusChanged( projectFullName, transaction.transferedSize / transaction.totalSize );
151✔
413

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

417
    // Send another request (or finish)
418
    downloadNextItem( projectFullName );
151✔
419
  }
151✔
420
  else
421
  {
422
    QString serverMsg = extractServerErrorMsg( r->readAll() );
1✔
423
    if ( serverMsg.isEmpty() )
1✔
424
    {
425
      serverMsg = r->errorString();
1✔
426
    }
427
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) );
1✔
428

429
    transaction.replyPullItem->deleteLater();
1✔
430
    transaction.replyPullItem = nullptr;
1✔
431

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

435
    if ( transaction.firstTimeDownload )
1✔
436
    {
437
      Q_ASSERT( !transaction.projectDir.isEmpty() );
1✔
438
      QDir( transaction.projectDir ).removeRecursively();
1✔
439
    }
440

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

444
    finishProjectSync( projectFullName, false );
1✔
445
  }
1✔
446
}
152✔
447

448
void MerginApi::cacheServerConfig()
39✔
449
{
450
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
39✔
451
  Q_ASSERT( r );
39✔
452

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

455
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
39✔
456
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
39✔
457
  Q_ASSERT( r == transaction.replyPullItem );
39✔
458

459
  if ( r->error() == QNetworkReply::NoError )
39✔
460
  {
461
    QByteArray data = r->readAll();
39✔
462

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

466
    transaction.replyPullItem->deleteLater();
39✔
467
    transaction.replyPullItem = nullptr;
39✔
468

469
    prepareDownloadConfig( projectFullName, true );
39✔
470
  }
39✔
471
  else
472
  {
473
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
474
    if ( serverMsg.isEmpty() )
×
475
    {
476
      serverMsg = r->errorString();
×
477
    }
478
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Failed to cache mergin config - %1. %2" ).arg( r->errorString(), serverMsg ) );
×
479

480
    transaction.replyPullItem->deleteLater();
×
481
    transaction.replyPullItem = nullptr;
×
482

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

486
    if ( transaction.firstTimeDownload )
×
487
    {
488
      CoreUtils::removeDir( transaction.projectDir );
×
489
    }
490

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

494
    finishProjectSync( projectFullName, false );
×
495
  }
×
496
}
39✔
497

498

499
void MerginApi::pushFile( const QString &projectFullName, const QString &transactionUUID, MerginFile file, int chunkNo )
152✔
500
{
501
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
152✔
502
  {
503
    return;
×
504
  }
505

506
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
152✔
507
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
152✔
508

509
  QString chunkID = file.chunks.at( chunkNo );
152✔
510

511
  QString filePath;
152✔
512
  if ( file.diffName.isEmpty() )
152✔
513
    filePath = transaction.projectDir + "/" + file.path;
140✔
514
  else  // use diff file instead of full file
515
    filePath = transaction.projectDir + "/.mergin/" + file.diffName;
12✔
516

517
  QFile f( filePath );
152✔
518
  QByteArray data;
152✔
519

520
  if ( f.open( QIODevice::ReadOnly ) )
152✔
521
  {
522
    f.seek( chunkNo * UPLOAD_CHUNK_SIZE );
152✔
523
    data = f.read( UPLOAD_CHUNK_SIZE );
152✔
524
  }
525

526
  QNetworkRequest request = getDefaultRequest();
152✔
527
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/push/chunk/%1/%2" ).arg( transactionUUID, chunkID ) );
304✔
528
  request.setUrl( url );
152✔
529
  request.setRawHeader( "Content-Type", "application/octet-stream" );
152✔
530
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
152✔
531

532
  Q_ASSERT( !transaction.replyPushFile );
152✔
533
  transaction.replyPushFile = mManager.post( request, data );
152✔
534
  connect( transaction.replyPushFile, &QNetworkReply::finished, this, &MerginApi::pushFileReplyFinished );
152✔
535

536
  CoreUtils::log( "push " + projectFullName, QStringLiteral( "Uploading item: " ) + url.toString() );
304✔
537
}
152✔
538

539
void MerginApi::pushStart( const QString &projectFullName, const QByteArray &json )
115✔
540
{
541
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
115✔
542
  {
543
    return;
×
544
  }
545

546
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
115✔
547
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
115✔
548

549
  QNetworkRequest request = getDefaultRequest();
115✔
550
  QUrl url( mApiRoot + QStringLiteral( "v1/project/push/%1" ).arg( projectFullName ) );
230✔
551
  request.setUrl( url );
115✔
552
  request.setRawHeader( "Content-Type", "application/json" );
115✔
553
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
115✔
554

555
  Q_ASSERT( !transaction.replyPushStart );
115✔
556
  transaction.replyPushStart = mManager.post( request, json );
115✔
557
  connect( transaction.replyPushStart, &QNetworkReply::finished, this, &MerginApi::pushStartReplyFinished );
115✔
558

559
  CoreUtils::log( "push " + projectFullName, QStringLiteral( "Starting push request: " ) + url.toString() );
230✔
560
}
115✔
561

562
void MerginApi::cancelPush( const QString &projectFullName )
2✔
563
{
564
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
2✔
565
  {
566
    return;
×
567
  }
568

569
  if ( !mTransactionalStatus.contains( projectFullName ) )
2✔
570
    return;
×
571

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

574
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
2✔
575

576
  // There is an open transaction, abort it followed by calling cancelUpload again.
577
  if ( transaction.replyPushProjectInfo )
2✔
578
  {
579
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Aborting project info request" ) );
1✔
580
    transaction.replyPushProjectInfo->abort();  // will trigger uploadInfoReplyFinished slot and emit sync finished
1✔
581
  }
582
  else if ( transaction.replyPushStart )
1✔
583
  {
584
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Aborting upload start" ) );
×
585
    transaction.replyPushStart->abort();  // will trigger uploadStartReplyFinished slot and emit sync finished
×
586
  }
587
  else if ( transaction.replyPushFile )
1✔
588
  {
589
    QString transactionUUID = transaction.transactionUUID;  // copy transaction uuid as the transaction object will be gone after abort
1✔
590
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Aborting upload file" ) );
1✔
591
    transaction.replyPushFile->abort();  // will trigger pushFileReplyFinished slot and emit sync finished
1✔
592

593
    // also need to cancel the transaction
594
    sendPushCancelRequest( projectFullName, transactionUUID );
1✔
595
  }
1✔
596
  else if ( transaction.replyPushFinish )
×
597
  {
598
    QString transactionUUID = transaction.transactionUUID;  // copy transaction uuid as the transaction object will be gone after abort
×
599
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Aborting upload finish" ) );
×
600
    transaction.replyPushFinish->abort();  // will trigger pushFinishReplyFinished slot and emit sync finished
×
601

602
    sendPushCancelRequest( projectFullName, transactionUUID );
×
603
  }
×
604
  else
605
  {
606
    Q_ASSERT( false );  // unexpected state
×
607
  }
608
}
609

610

611
void MerginApi::sendPushCancelRequest( const QString &projectFullName, const QString &transactionUUID )
1✔
612
{
613
  QNetworkRequest request = getDefaultRequest();
1✔
614
  QUrl url( mApiRoot + QStringLiteral( "v1/project/push/cancel/%1" ).arg( transactionUUID ) );
2✔
615
  request.setUrl( url );
1✔
616
  request.setRawHeader( "Content-Type", "application/json" );
1✔
617
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
1✔
618

619
  QNetworkReply *reply = mManager.post( request, QByteArray() );
1✔
620
  connect( reply, &QNetworkReply::finished, this, &MerginApi::pushCancelReplyFinished );
1✔
621
  CoreUtils::log( "push " + projectFullName, QStringLiteral( "Requesting upload transaction cancel: " ) + url.toString() );
2✔
622
}
1✔
623

624
void MerginApi::cancelPull( const QString &projectFullName )
2✔
625
{
626
  if ( !mTransactionalStatus.contains( projectFullName ) )
2✔
627
    return;
×
628

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

631
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
2✔
632

633
  if ( transaction.replyPullProjectInfo )
2✔
634
  {
635
    // we're still fetching project info
636
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Aborting project info request" ) );
1✔
637
    transaction.replyPullProjectInfo->abort();  // abort will trigger pullInfoReplyFinished() slot
1✔
638
  }
639
  else if ( transaction.replyPullItem )
1✔
640
  {
641
    // we're already downloading some files
642
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Aborting pending download" ) );
1✔
643
    transaction.replyPullItem->abort();  // abort will trigger downloadItemReplyFinished slot
1✔
644
  }
645
  else
646
  {
647
    Q_ASSERT( false );  // unexpected state
×
648
  }
649
}
650

651
void MerginApi::pushFinish( const QString &projectFullName, const QString &transactionUUID )
110✔
652
{
653
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
110✔
654
  {
655
    return;
×
656
  }
657

658
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
110✔
659
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
110✔
660

661
  QNetworkRequest request = getDefaultRequest();
110✔
662
  QUrl url( mApiRoot + QStringLiteral( "v1/project/push/finish/%1" ).arg( transactionUUID ) );
220✔
663
  request.setUrl( url );
110✔
664
  request.setRawHeader( "Content-Type", "application/json" );
110✔
665
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
110✔
666

667
  Q_ASSERT( !transaction.replyPushFinish );
110✔
668
  transaction.replyPushFinish = mManager.post( request, QByteArray() );
110✔
669
  connect( transaction.replyPushFinish, &QNetworkReply::finished, this, &MerginApi::pushFinishReplyFinished );
110✔
670

671
  CoreUtils::log( "push " + projectFullName, QStringLiteral( "Requesting transaction finish: " ) + transactionUUID );
110✔
672
}
110✔
673

674
bool MerginApi::pullProject( const QString &projectNamespace, const QString &projectName, bool withAuth )
100✔
675
{
676
  QString projectFullName = getFullProjectName( projectNamespace, projectName );
200✔
677
  bool pullHasStarted = false;
100✔
678

679
  CoreUtils::log( "pull " + projectFullName, "### Starting ###" );
100✔
680

681
  QNetworkReply *reply = getProjectInfo( projectFullName, withAuth );
100✔
682
  if ( reply )
100✔
683
  {
684
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Requesting project info: " ) + reply->request().url().toString() );
200✔
685

686
    Q_ASSERT( !mTransactionalStatus.contains( projectFullName ) );
100✔
687
    mTransactionalStatus.insert( projectFullName, TransactionStatus() );
100✔
688
    mTransactionalStatus[projectFullName].replyPullProjectInfo = reply;
100✔
689
    mTransactionalStatus[projectFullName].configAllowed = mSupportsSelectiveSync;
100✔
690
    mTransactionalStatus[projectFullName].type = TransactionStatus::Pull;
100✔
691

692
    emit syncProjectStatusChanged( projectFullName, 0 );
100✔
693

694
    connect( reply, &QNetworkReply::finished, this, &MerginApi::pullInfoReplyFinished );
100✔
695
    pullHasStarted = true;
100✔
696
  }
697
  else
698
  {
699
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "FAILED to create project info request!" ) );
×
700
  }
701

702
  return pullHasStarted;
100✔
703
}
100✔
704

705
bool MerginApi::pushProject( const QString &projectNamespace, const QString &projectName, bool isInitialPush )
143✔
706
{
707
  QString projectFullName = getFullProjectName( projectNamespace, projectName );
286✔
708
  bool pushHasStarted = false;
143✔
709

710
  CoreUtils::log( "push " + projectFullName, "### Starting ###" );
143✔
711

712
  QNetworkReply *reply = getProjectInfo( projectFullName );
143✔
713
  if ( reply )
143✔
714
  {
715
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Requesting project info: " ) + reply->request().url().toString() );
286✔
716

717
    // create entry about pending upload for the project
718
    Q_ASSERT( !mTransactionalStatus.contains( projectFullName ) );
143✔
719
    mTransactionalStatus.insert( projectFullName, TransactionStatus() );
143✔
720
    mTransactionalStatus[projectFullName].replyPushProjectInfo = reply;
143✔
721
    mTransactionalStatus[projectFullName].isInitialPush = isInitialPush;
143✔
722
    mTransactionalStatus[projectFullName].configAllowed = mSupportsSelectiveSync;
143✔
723
    mTransactionalStatus[projectFullName].type = TransactionStatus::Push;
143✔
724

725
    emit syncProjectStatusChanged( projectFullName, 0 );
143✔
726

727
    connect( reply, &QNetworkReply::finished, this, &MerginApi::pushInfoReplyFinished );
143✔
728
    pushHasStarted = true;
143✔
729
  }
730
  else
731
  {
732
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED to create project info request!" ) );
×
733
  }
734

735
  return pushHasStarted;
143✔
736
}
143✔
737

738
void MerginApi::authorize( const QString &login, const QString &password )
8✔
739
{
740
  if ( login.isEmpty() || password.isEmpty() )
8✔
741
  {
742
    emit authFailed();
×
743
    emit notify( QStringLiteral( "Please enter your login details" ) );
×
744
    return;
×
745
  }
746

747
  mUserAuth->blockSignals( true );
8✔
748
  mUserAuth->setPassword( password );
8✔
749
  mUserAuth->blockSignals( false );
8✔
750

751
  QNetworkRequest request = getDefaultRequest( false );
8✔
752
  QString urlString = mApiRoot + QStringLiteral( "v1/auth/login" );
16✔
753
  QUrl url( urlString );
8✔
754
  request.setUrl( url );
8✔
755
  request.setRawHeader( "Content-Type", "application/json" );
8✔
756

757
  QJsonDocument jsonDoc;
8✔
758
  QJsonObject jsonObject;
8✔
759
  jsonObject.insert( QStringLiteral( "login" ), login );
16✔
760
  jsonObject.insert( QStringLiteral( "password" ), mUserAuth->password() );
16✔
761
  jsonDoc.setObject( jsonObject );
8✔
762
  QByteArray json = jsonDoc.toJson( QJsonDocument::Compact );
8✔
763

764
  QNetworkReply *reply = mManager.post( request, json );
8✔
765
  connect( reply, &QNetworkReply::finished, this, &MerginApi::authorizeFinished );
8✔
766
  CoreUtils::log( "auth", QStringLiteral( "Requesting authorization: " ) + url.toString() );
16✔
767
}
8✔
768

769
void MerginApi::registerUser( const QString &username,
8✔
770
                              const QString &email,
771
                              const QString &password,
772
                              const QString &confirmPassword,
773
                              bool acceptedTOC )
774
{
775
  // Some very basic checks, so we do not validate everything
776
  if ( username.isEmpty() || username.length() < 4 )
8✔
777
  {
778
    QString msg = tr( "Username must have at least 4 characters" );
1✔
779
    emit registrationFailed( msg, RegistrationError::RegistrationErrorType::USERNAME );
1✔
780
    return;
1✔
781
  }
1✔
782

783
  if ( !CoreUtils::isValidName( username ) )
7✔
784
  {
785
    QString msg = tr( "Username contains invalid characters" );
×
786
    emit registrationFailed( msg, RegistrationError::RegistrationErrorType::USERNAME );
×
787
    return;
×
788
  }
×
789

790
  if ( email.isEmpty() || !email.contains( '@' ) || !email.contains( '.' ) )
7✔
791
  {
792
    QString msg = tr( "Please enter a valid email" );
1✔
793
    emit registrationFailed( msg, RegistrationError::RegistrationErrorType::EMAIL );
1✔
794
    return;
1✔
795
  }
1✔
796

797
  if ( password.isEmpty() || password.length() < 8 )
6✔
798
  {
799
    QString msg = tr( "Password not strong enough. It must"
1✔
800
                      "%1 be at least 8 characters long"
801
                      "%1 contain lowercase characters"
802
                      "%1 contain uppercase characters"
803
                      "%1 contain digits or special characters" )
804
                  .arg( "<br />  -" );
2✔
805
    emit registrationFailed( msg, RegistrationError::RegistrationErrorType::PASSWORD );
1✔
806
    return;
1✔
807

808
  }
1✔
809

810
  if ( confirmPassword != password )
5✔
811
  {
812
    QString msg = tr( "Passwords do not match" );
1✔
813
    emit registrationFailed( msg, RegistrationError::RegistrationErrorType::CONFIRM_PASSWORD );
1✔
814
    return;
1✔
815
  }
1✔
816

817
  if ( !acceptedTOC )
4✔
818
  {
819
    QString msg = tr( "Please accept Terms and Privacy Policy" );
1✔
820
    emit registrationFailed( msg, RegistrationError::RegistrationErrorType::TOC );
1✔
821
    return;
1✔
822
  }
1✔
823

824
  // request
825
  QNetworkRequest request = getDefaultRequest( false );
3✔
826
  QString urlString = mApiRoot + QStringLiteral( "v1/auth/register" );
6✔
827
  QUrl url( urlString );
3✔
828
  request.setUrl( url );
3✔
829
  request.setRawHeader( "Content-Type", "application/json" );
3✔
830

831
  QJsonDocument jsonDoc;
3✔
832
  QJsonObject jsonObject;
3✔
833
  jsonObject.insert( QStringLiteral( "username" ), username );
6✔
834
  jsonObject.insert( QStringLiteral( "email" ), email );
6✔
835
  jsonObject.insert( QStringLiteral( "password" ), password );
6✔
836
  jsonObject.insert( QStringLiteral( "api_key" ), getApiKey( mApiRoot ) );
6✔
837
  jsonDoc.setObject( jsonObject );
3✔
838
  QByteArray json = jsonDoc.toJson( QJsonDocument::Compact );
3✔
839
  QNetworkReply *reply = mManager.post( request, json );
3✔
840
  connect( reply, &QNetworkReply::finished, this, [ = ]() { this->registrationFinished( username, password ); } );
6✔
841
  CoreUtils::log( "auth", QStringLiteral( "Requesting registration: " ) + url.toString() );
6✔
842
}
3✔
843

844
void MerginApi::getUserInfo()
20✔
845
{
846
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
20✔
847
  {
848
    return;
×
849
  }
850

851
  QString urlString;
20✔
852
  if ( mServerType == MerginServerType::OLD )
20✔
853
  {
854
    urlString = mApiRoot + QStringLiteral( "v1/user/%1" ).arg( mUserAuth->username() );
×
855
  }
856
  else
857
  {
858
    urlString = mApiRoot + QStringLiteral( "v1/user/profile" );
20✔
859
  }
860

861
  QNetworkRequest request = getDefaultRequest();
20✔
862
  QUrl url( urlString );
20✔
863
  request.setUrl( url );
20✔
864

865
  QNetworkReply *reply = mManager.get( request );
20✔
866
  CoreUtils::log( "user info", QStringLiteral( "Requesting user info: " ) + url.toString() );
40✔
867
  connect( reply, &QNetworkReply::finished, this, &MerginApi::getUserInfoFinished );
20✔
868
}
20✔
869

870
void MerginApi::getWorkspaceInfo()
25✔
871
{
872
  if ( mServerType == MerginServerType::OLD )
25✔
873
  {
874
    return;
×
875
  }
876

877
  if ( mUserInfo->activeWorkspaceId() == -1 )
25✔
878
  {
879
    return;
×
880
  }
881

882
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
25✔
883
  {
884
    return;
×
885
  }
886

887
  QString urlString = mApiRoot + QStringLiteral( "v1/workspace/%1" ).arg( mUserInfo->activeWorkspaceId() );
50✔
888
  QNetworkRequest request = getDefaultRequest();
25✔
889
  QUrl url( urlString );
25✔
890
  request.setUrl( url );
25✔
891

892
  QNetworkReply *reply = mManager.get( request );
25✔
893
  CoreUtils::log( "workspace info", QStringLiteral( "Requesting workspace info: " ) + url.toString() );
50✔
894
  connect( reply, &QNetworkReply::finished, this, &MerginApi::getWorkspaceInfoReplyFinished );
25✔
895
}
25✔
896

897
void MerginApi::getServiceInfo()
34✔
898
{
899
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
34✔
900
  {
901
    return;
×
902
  }
903

904
  QString urlString;
34✔
905

906
  if ( mServerType == MerginServerType::SAAS )
34✔
907
  {
908
    urlString = mApiRoot + QStringLiteral( "v1/workspace/%1/service" ).arg( mUserInfo->activeWorkspaceId() );
34✔
909
  }
910
  else if ( mServerType == MerginServerType::OLD )
×
911
  {
912
    urlString = mApiRoot + QStringLiteral( "v1/user/service" );
×
913
  }
914
  else
915
  {
916
    return;
×
917
  }
918

919
  QNetworkRequest request = getDefaultRequest( true );
34✔
920
  QUrl url( urlString );
34✔
921
  request.setUrl( url );
34✔
922

923
  QNetworkReply *reply = mManager.get( request );
34✔
924

925
  connect( reply, &QNetworkReply::finished, this, &MerginApi::getServiceInfoReplyFinished );
34✔
926

927
  CoreUtils::log( "Service info", QStringLiteral( "Requesting service info: " ) + url.toString() );
68✔
928
}
34✔
929

930
void MerginApi::getServiceInfoReplyFinished()
33✔
931
{
932
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
33✔
933
  Q_ASSERT( r );
33✔
934

935
  if ( r->error() == QNetworkReply::NoError )
33✔
936
  {
937
    CoreUtils::log( "Service info", QStringLiteral( "Success" ) );
33✔
938

939
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
33✔
940
    if ( doc.isObject() )
33✔
941
    {
942
      QJsonObject docObj = doc.object();
33✔
943
      mSubscriptionInfo->setFromJson( docObj );
33✔
944
    }
33✔
945
  }
33✔
946
  else
947
  {
948
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
949
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "getServiceInfo" ), r->errorString(), serverMsg );
×
950
    CoreUtils::log( "Service info", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
951

952
    mSubscriptionInfo->clear();
×
953

954
    int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
×
955
    if ( httpCode == 404 )
×
956
    {
957
      // no such API on the server, do not emit anything
958
    }
959
    else if ( httpCode == 403 )
×
960
    {
961
      // forbidden - I do not have enough rights to see this, do not emit anything
962
    }
963
    else
964
    {
965
      emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: getServiceInfo" ) );
×
966
    }
967
  }
×
968

969
  r->deleteLater();
33✔
970
}
33✔
971

972
void MerginApi::clearAuth()
3✔
973
{
974
  mUserAuth->clear();
3✔
975
  mUserInfo->clear();
3✔
976
  mUserInfo->clearCachedWorkspacesInfo();
3✔
977
  mWorkspaceInfo->clear();
3✔
978
  mSubscriptionInfo->clear();
3✔
979
}
3✔
980

981
void MerginApi::resetApiRoot()
×
982
{
983
  QSettings settings;
×
984
  settings.beginGroup( QStringLiteral( "Input/" ) );
×
985
  setApiRoot( defaultApiRoot() );
×
986
  settings.endGroup();
×
987
}
×
988

989
QString MerginApi::resetPasswordUrl()
×
990
{
991
  if ( !mApiRoot.isEmpty() )
×
992
  {
993
    QUrl base( mApiRoot );
×
994
    return base.resolved( QUrl( "login/reset" ) ).toString();
×
995
  }
×
996
  return QString();
×
997
}
998

999
bool MerginApi::createProject( const QString &projectNamespace, const QString &projectName, bool isPublic )
40✔
1000
{
1001
  if ( !validateAuth() )
40✔
1002
  {
1003
    emit missingAuthorizationError( projectName );
×
1004
    return false;
×
1005
  }
1006

1007
  if ( mApiVersionStatus != MerginApiStatus::OK )
40✔
1008
  {
1009
    return false;
×
1010
  }
1011

1012
  QString projectFullName = getFullProjectName( projectNamespace, projectName );
80✔
1013

1014
  QNetworkRequest request = getDefaultRequest();
40✔
1015
  QUrl url( mApiRoot + QString( "/v1/project/%1" ).arg( projectNamespace ) );
80✔
1016
  request.setUrl( url );
40✔
1017
  request.setRawHeader( "Content-Type", "application/json" );
40✔
1018
  request.setRawHeader( "Accept", "application/json" );
40✔
1019
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
40✔
1020

1021
  QJsonDocument jsonDoc;
40✔
1022
  QJsonObject jsonObject;
40✔
1023
  jsonObject.insert( QStringLiteral( "name" ), projectName );
80✔
1024
  jsonObject.insert( QStringLiteral( "public" ), isPublic );
80✔
1025
  jsonDoc.setObject( jsonObject );
40✔
1026
  QByteArray json = jsonDoc.toJson( QJsonDocument::Compact );
40✔
1027

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

1032
  return true;
40✔
1033
}
40✔
1034

1035
void MerginApi::deleteProject( const QString &projectNamespace, const QString &projectName, bool informUser )
43✔
1036
{
1037
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
43✔
1038
  {
1039
    return;
×
1040
  }
1041

1042
  QString projectFullName = getFullProjectName( projectNamespace, projectName );
86✔
1043

1044
  QNetworkRequest request = getDefaultRequest();
43✔
1045
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/%1" ).arg( projectFullName ) );
86✔
1046
  request.setUrl( url );
43✔
1047
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
43✔
1048
  QNetworkReply *reply = mManager.deleteResource( request );
43✔
1049
  connect( reply, &QNetworkReply::finished, this, [this, informUser]() { this->deleteProjectFinished( informUser );} );
86✔
1050
  CoreUtils::log( "delete " + projectFullName, QStringLiteral( "Requesting project deletion: " ) + url.toString() );
86✔
1051
}
43✔
1052

1053
void MerginApi::saveAuthData()
12✔
1054
{
1055
  QSettings settings;
12✔
1056
  settings.beginGroup( "Input/" );
12✔
1057
  settings.setValue( "apiRoot", mApiRoot );
12✔
1058
  settings.endGroup();
12✔
1059

1060
  mUserAuth->saveAuthData();
12✔
1061
  mUserInfo->clear();
12✔
1062
}
12✔
1063

1064
void MerginApi::createProjectFinished()
40✔
1065
{
1066
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
40✔
1067
  Q_ASSERT( r );
40✔
1068

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

1071
  QString projectNamespace, projectName;
40✔
1072
  extractProjectName( projectFullName, projectNamespace, projectName );
40✔
1073

1074
  if ( r->error() == QNetworkReply::NoError )
40✔
1075
  {
1076
    CoreUtils::log( "create " + projectFullName, QStringLiteral( "Success" ) );
39✔
1077
    emit projectCreated( projectFullName, true );
39✔
1078

1079

1080
    // Upload data if createProject has been called for a local project with empty namespace (case of migrating a project)
1081
    for ( const LocalProject &info : mLocalProjects.projects() )
433✔
1082
    {
1083
      if ( info.projectName == projectName && info.projectNamespace.isEmpty() )
394✔
1084
      {
1085
        mLocalProjects.updateNamespace( info.projectDir, projectNamespace );
3✔
1086
        emit projectAttachedToMergin( projectFullName, projectName );
3✔
1087

1088
        QDir projectDir( info.projectDir );
3✔
1089
        if ( projectDir.exists() && !projectDir.isEmpty() )
3✔
1090
        {
1091
          pushProject( projectNamespace, projectName, true );
3✔
1092
        }
1093
      }
3✔
1094
    }
39✔
1095
  }
1096
  else
1097
  {
1098
    QByteArray data = r->readAll();
1✔
1099
    QString code = extractServerErrorCode( data );
1✔
1100
    QString serverMsg = extractServerErrorMsg( data );
1✔
1101
    QString message = QStringLiteral( "FAILED - %1: %2" ).arg( r->errorString(), serverMsg );
2✔
1102
    bool showLimitReachedDialog = EnumHelper::isEqual( code, ErrorCode::ProjectsLimitHit );
1✔
1103

1104
    CoreUtils::log( "create " + projectFullName, message );
1✔
1105

1106
    emit projectCreated( projectFullName, false );
1✔
1107

1108
    if ( showLimitReachedDialog )
1✔
1109
    {
1110
      int maxProjects = 0;
×
1111
      QVariant maxProjectVariant = extractServerErrorValue( data, "projects_quota" );
×
1112
      if ( maxProjectVariant.isValid() )
×
1113
        maxProjects = maxProjectVariant.toInt();
×
1114
      emit projectLimitReached( maxProjects, serverMsg );
×
1115
    }
×
1116
    else
1117
    {
1118
      int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
1✔
1119
      emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: createProject" ), httpCode, projectName );
1✔
1120
    }
1121
  }
1✔
1122
  r->deleteLater();
40✔
1123
}
40✔
1124

1125
void MerginApi::deleteProjectFinished( bool informUser )
43✔
1126
{
1127
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
43✔
1128
  Q_ASSERT( r );
43✔
1129

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

1132
  if ( r->error() == QNetworkReply::NoError )
43✔
1133
  {
1134
    CoreUtils::log( "delete " + projectFullName, QStringLiteral( "Success" ) );
2✔
1135

1136
    if ( informUser )
2✔
1137
      emit notify( QStringLiteral( "Project deleted" ) );
2✔
1138

1139
    emit serverProjectDeleted( projectFullName, true );
2✔
1140
  }
1141
  else
1142
  {
1143
    QString serverMsg = extractServerErrorMsg( r->readAll() );
82✔
1144
    CoreUtils::log( "delete " + projectFullName, QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) );
41✔
1145
    emit serverProjectDeleted( projectFullName, false );
41✔
1146
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: deleteProject" ) );
82✔
1147
  }
41✔
1148
  r->deleteLater();
43✔
1149
}
43✔
1150

1151
void MerginApi::authorizeFinished()
8✔
1152
{
1153
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
8✔
1154
  Q_ASSERT( r );
8✔
1155

1156
  if ( r->error() == QNetworkReply::NoError )
8✔
1157
  {
1158
    CoreUtils::log( "auth", QStringLiteral( "Success" ) );
8✔
1159
    const QByteArray data = r->readAll();
8✔
1160
    QJsonDocument doc = QJsonDocument::fromJson( data );
8✔
1161
    if ( doc.isObject() )
8✔
1162
    {
1163
      QJsonObject docObj = doc.object();
8✔
1164
      mUserAuth->setFromJson( docObj );
8✔
1165
    }
8✔
1166
    else
1167
    {
1168
      // keep username and password, but clear token
1169
      // this is problem with internet connection or server
1170
      // so do not force user to input login credentials again
1171
      mUserAuth->clearTokenData();
×
1172
      emit authFailed();
×
1173
      CoreUtils::log( "Auth", QStringLiteral( "FAILED - invalid JSON response" ) );
×
1174
      emit notify( "Internal server error during authorization" );
×
1175
    }
1176
  }
8✔
1177
  else
1178
  {
1179
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
1180
    QVariant statusCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute );
×
1181
    int status = statusCode.toInt();
×
1182
    CoreUtils::log( "Auth", QStringLiteral( "FAILED - %1. %2 (%3)" ).arg( r->errorString(), serverMsg, QString::number( status ) ) );
×
1183

1184
    if ( status == 401 )
×
1185
    {
1186
      // OK, we have INVALID username or password or
1187
      // our user got blocked on the server by admin or owner
1188
      // lets show error to user and let him try different credentials
1189
      emit authFailed();
×
1190
      emit notify( serverMsg );
×
1191

1192
      mUserAuth->blockSignals( true );
×
1193
      mUserAuth->setUsername( QString() );
×
1194
      mUserAuth->setPassword( QString() );
×
1195
      mUserAuth->blockSignals( false );
×
1196

1197
    }
1198
    else
1199
    {
1200
      // keep username and password
1201
      // this is problem with internet connection or server
1202
      // so do not force user to input login credentials again
1203
      emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: authorize" ) );
×
1204
    }
1205

1206
    // in case of any error, just clean token and request new one
1207
    mUserAuth->clearTokenData();
×
1208
  }
×
1209

1210
  if ( mAuthLoopEvent.isRunning() )
8✔
1211
  {
1212
    mAuthLoopEvent.exit();
1✔
1213
  }
1214
  r->deleteLater();
8✔
1215
}
8✔
1216

1217
void MerginApi::registrationFinished( const QString &username, const QString &password )
3✔
1218
{
1219
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
3✔
1220
  Q_ASSERT( r );
3✔
1221

1222
  if ( r->error() == QNetworkReply::NoError )
3✔
1223
  {
1224
    CoreUtils::log( "register", QStringLiteral( "Success" ) );
3✔
1225
    QString msg = tr( "Registration successful" );
3✔
1226
    emit notify( msg );
3✔
1227

1228
    if ( !username.isEmpty() && !password.isEmpty() ) // log in immediately
3✔
1229
      authorize( username, password );
3✔
1230

1231
    emit registrationSucceeded();
3✔
1232
  }
3✔
1233
  else
1234
  {
1235
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
1236
    CoreUtils::log( "register", QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) );
×
1237
    QVariant statusCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute );
×
1238
    int status = statusCode.toInt();
×
1239
    if ( status == 401 || status == 400 )
×
1240
    {
1241
      emit registrationFailed( serverMsg, RegistrationError::RegistrationErrorType::OTHER );
×
1242
      emit notify( serverMsg );
×
1243
    }
1244
    else if ( status == 404 )
×
1245
    {
1246
      // the self-registration is not allowed on the server
1247
      QString msg = tr( "New registrations are not allowed on the selected Mergin server.%1Please check with your administrator." ).arg( "\n" );
×
1248
      emit registrationFailed( msg, RegistrationError::RegistrationErrorType::OTHER );
×
1249
      emit notify( msg );
×
1250
    }
×
1251
    else
1252
    {
1253
      QString msg = QStringLiteral( "Mergin API error: register" );
×
1254
      emit registrationFailed( msg, RegistrationError::RegistrationErrorType::OTHER );
×
1255
      emit networkErrorOccurred( serverMsg, msg );
×
1256
    }
×
1257
  }
×
1258
  r->deleteLater();
3✔
1259
}
3✔
1260

1261
void MerginApi::pingMerginReplyFinished()
11✔
1262
{
1263
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
11✔
1264
  Q_ASSERT( r );
11✔
1265
  QString apiVersion;
11✔
1266
  QString serverMsg;
11✔
1267
  bool serverSupportsSubscriptions = false;
11✔
1268

1269
  if ( r->error() == QNetworkReply::NoError )
11✔
1270
  {
1271
    CoreUtils::log( "ping", QStringLiteral( "Success" ) );
11✔
1272
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
11✔
1273
    if ( doc.isObject() )
11✔
1274
    {
1275
      QJsonObject obj = doc.object();
11✔
1276
      apiVersion = obj.value( QStringLiteral( "version" ) ).toString();
11✔
1277
      serverSupportsSubscriptions = obj.value( QStringLiteral( "subscriptions_enabled" ) ).toBool();
11✔
1278
    }
11✔
1279
  }
11✔
1280
  else
1281
  {
1282
    serverMsg = extractServerErrorMsg( r->readAll() );
×
1283
    CoreUtils::log( "ping", QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) );
×
1284
  }
1285
  r->deleteLater();
11✔
1286
  emit pingMerginFinished( apiVersion, serverSupportsSubscriptions, serverMsg );
11✔
1287
}
11✔
1288

1289
void MerginApi::onPlanProductIdChanged()
15✔
1290
{
1291
  if ( mUserAuth->hasAuthData() )
15✔
1292
  {
1293
    if ( mServerType == MerginServerType::OLD )
14✔
1294
    {
1295
      getUserInfo();
×
1296
    }
1297
    else
1298
    {
1299
      getWorkspaceInfo();
14✔
1300
    }
1301
  }
1302
}
15✔
1303

1304
QNetworkReply *MerginApi::getProjectInfo( const QString &projectFullName, bool withAuth )
243✔
1305
{
1306
  if ( withAuth && !validateAuth() )
243✔
1307
  {
1308
    emit missingAuthorizationError( projectFullName );
×
1309
    return nullptr;
×
1310
  }
1311

1312
  if ( mApiVersionStatus != MerginApiStatus::OK )
243✔
1313
  {
1314
    return nullptr;
×
1315
  }
1316

1317
  int sinceVersion = -1;
243✔
1318
  LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName );
243✔
1319
  if ( projectInfo.isValid() )
243✔
1320
  {
1321
    // let's also fetch the recent history of diffable files
1322
    // (the "since" is inclusive, so if we are on v2, we want to use since=v3 which will include v2->v3, v3->v4, ...)
1323
    sinceVersion = projectInfo.localVersion + 1;
180✔
1324
  }
1325

1326
  QUrlQuery query;
243✔
1327
  if ( sinceVersion != -1 )
243✔
1328
    query.addQueryItem( QStringLiteral( "since" ), QStringLiteral( "v%1" ).arg( sinceVersion ) );
360✔
1329

1330
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/%1" ).arg( projectFullName ) );
486✔
1331
  url.setQuery( query );
243✔
1332

1333
  QNetworkRequest request = getDefaultRequest( withAuth );
243✔
1334
  request.setUrl( url );
243✔
1335
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
243✔
1336

1337
  return mManager.get( request );
243✔
1338
}
243✔
1339

1340
bool MerginApi::validateAuth()
816✔
1341
{
1342
  if ( !mUserAuth->hasAuthData() )
816✔
1343
  {
1344
    emit authRequested();
×
1345
    return false;
×
1346
  }
1347

1348
  if ( mUserAuth->authToken().isEmpty() || mUserAuth->tokenExpiration() < QDateTime().currentDateTime().toUTC() )
816✔
1349
  {
1350
    authorize( mUserAuth->username(), mUserAuth->password() );
1✔
1351
    CoreUtils::log( QStringLiteral( "MerginApi" ), QStringLiteral( "Requesting authorization because of missing or expired token." ) );
2✔
1352
    mAuthLoopEvent.exec();
1✔
1353
  }
1354
  return true;
816✔
1355
}
1356

1357
void MerginApi::checkMerginVersion( QString apiVersion, bool serverSupportsSubscriptions, QString msg )
11✔
1358
{
1359
  setApiSupportsSubscriptions( serverSupportsSubscriptions );
11✔
1360

1361
  if ( msg.isEmpty() )
11✔
1362
  {
1363
    int major = -1;
11✔
1364
    int minor = -1;
11✔
1365
    QRegularExpression re;
11✔
1366
    re.setPattern( QStringLiteral( "(?<major>\\d+)[.](?<minor>\\d+)" ) );
11✔
1367
    QRegularExpressionMatch match = re.match( apiVersion );
11✔
1368
    if ( match.hasMatch() )
11✔
1369
    {
1370
      major = match.captured( "major" ).toInt();
11✔
1371
      minor = match.captured( "minor" ).toInt();
11✔
1372
    }
1373

1374
    if ( ( MERGIN_API_VERSION_MAJOR == major && MERGIN_API_VERSION_MINOR <= minor ) || ( MERGIN_API_VERSION_MAJOR < major ) )
11✔
1375
    {
1376
      setApiVersionStatus( MerginApiStatus::OK );
11✔
1377
    }
1378
    else
1379
    {
1380
      setApiVersionStatus( MerginApiStatus::INCOMPATIBLE );
×
1381
    }
1382
  }
11✔
1383
  else
1384
  {
1385
    setApiVersionStatus( MerginApiStatus::NOT_FOUND );
×
1386
  }
1387
}
11✔
1388

1389
bool MerginApi::extractProjectName( const QString &sourceString, QString &projectNamespace, QString &name )
189✔
1390
{
1391
  QStringList parts = sourceString.split( "/" );
189✔
1392
  if ( parts.length() > 1 )
189✔
1393
  {
1394
    projectNamespace = parts.at( parts.length() - 2 );
189✔
1395
    name = parts.last();
189✔
1396
    return true;
189✔
1397
  }
1398
  else
1399
  {
1400
    name = sourceString;
×
1401
    return false;
×
1402
  }
1403
}
189✔
1404

1405
QString MerginApi::extractServerErrorCode( const QByteArray &data )
1✔
1406
{
1407
  QVariant code = extractServerErrorValue( data, QStringLiteral( "code" ) );
2✔
1408
  if ( code.isValid() )
1✔
1409
    return code.toString();
×
1410
  return QString();
1✔
1411
}
1✔
1412

1413
QVariant MerginApi::extractServerErrorValue( const QByteArray &data, const QString &key )
1✔
1414
{
1415
  QJsonDocument doc = QJsonDocument::fromJson( data );
1✔
1416
  if ( doc.isObject() )
1✔
1417
  {
1418
    QJsonObject obj = doc.object();
1✔
1419
    if ( obj.contains( key ) )
1✔
1420
    {
1421
      QJsonValue val = obj.value( key );
×
1422
      return val.toVariant();
×
1423
    }
×
1424
  }
1✔
1425

1426
  return QVariant();
1✔
1427
}
1✔
1428

1429
QString MerginApi::extractServerErrorMsg( const QByteArray &data )
50✔
1430
{
1431
  QString serverMsg = "[can't parse server error]";
50✔
1432
  QJsonDocument doc = QJsonDocument::fromJson( data );
50✔
1433
  if ( doc.isObject() )
50✔
1434
  {
1435
    QJsonObject obj = doc.object();
45✔
1436
    if ( obj.contains( QStringLiteral( "detail" ) ) )
45✔
1437
    {
1438
      QJsonValue vDetail = obj.value( "detail" );
43✔
1439
      if ( vDetail.isString() )
43✔
1440
      {
1441
        serverMsg = vDetail.toString();
43✔
1442
      }
1443
      else if ( vDetail.isObject() )
×
1444
      {
1445
        serverMsg = QJsonDocument( vDetail.toObject() ).toJson();
×
1446
      }
1447
    }
43✔
1448
    else if ( obj.contains( QStringLiteral( "name" ) ) )
2✔
1449
    {
1450
      QJsonValue val = obj.value( "name" );
2✔
1451
      if ( val.isArray() )
2✔
1452
      {
1453
        QJsonArray errors = val.toArray();
1✔
1454
        QStringList messages;
1✔
1455
        for ( auto it = errors.constBegin(); it != errors.constEnd(); ++it )
2✔
1456
        {
1457
          messages << it->toString();
1✔
1458
        }
1459
        serverMsg = messages.join( " " );
1✔
1460
      }
1✔
1461
    }
2✔
1462
    else
1463
    {
1464
      serverMsg = "[can't parse server error]";
×
1465
    }
1466
  }
45✔
1467
  else
1468
  {
1469
    // take only first 1000 bytes of the message ~ there are situations when data is an unclosed string that would eat the whole log memory
1470
    serverMsg = data.mid( 0, 1000 );
5✔
1471
  }
1472

1473
  return serverMsg;
100✔
1474
}
50✔
1475

1476

1477
LocalProject MerginApi::getLocalProject( const QString &projectFullName )
7✔
1478
{
1479
  return mLocalProjects.projectFromMerginName( projectFullName );
7✔
1480
}
1481

1482
ProjectDiff MerginApi::localProjectChanges( const QString &projectDir )
17✔
1483
{
1484
  MerginProjectMetadata projectMetadata = MerginProjectMetadata::fromCachedJson( projectDir + "/" + sMetadataFile );
34✔
1485
  QList<MerginFile> localFiles = getLocalProjectFiles( projectDir + "/" );
17✔
1486

1487
  MerginConfig config = MerginConfig::fromFile( projectDir + "/" + sMerginConfigFile );
34✔
1488

1489
  return compareProjectFiles( projectMetadata.files, projectMetadata.files, localFiles, projectDir, config.isValid, config );
34✔
1490
}
17✔
1491

1492
bool MerginApi::hasLocalProjectChanges( const QString &projectDir )
328✔
1493
{
1494
  MerginProjectMetadata projectMetadata = MerginProjectMetadata::fromCachedJson( projectDir + "/" + sMetadataFile );
656✔
1495
  QList<MerginFile> localFiles = getLocalProjectFiles( projectDir + "/" );
328✔
1496

1497
  MerginConfig config = MerginConfig::fromFile( projectDir + "/" + sMerginConfigFile );
656✔
1498

1499
  return hasLocalChanges( projectMetadata.files, localFiles, projectDir );
656✔
1500
}
328✔
1501

1502
QString MerginApi::getTempProjectDir( const QString &projectFullName )
337✔
1503
{
1504
  return mDataDir + "/" + TEMP_FOLDER + projectFullName;
674✔
1505
}
1506

1507
QString MerginApi::getFullProjectName( QString projectNamespace, QString projectName ) // TODO: move to inpututils?
43,978✔
1508
{
1509
  return QString( "%1/%2" ).arg( projectNamespace ).arg( projectName );
43,978✔
1510
}
1511

1512
MerginApiStatus::VersionStatus MerginApi::apiVersionStatus() const
38✔
1513
{
1514
  return mApiVersionStatus;
38✔
1515
}
1516

1517
void MerginApi::setApiVersionStatus( const MerginApiStatus::VersionStatus &apiVersionStatus )
35✔
1518
{
1519
  if ( mApiVersionStatus != apiVersionStatus )
35✔
1520
  {
1521
    mApiVersionStatus = apiVersionStatus;
33✔
1522
    emit apiVersionStatusChanged();
33✔
1523
  }
1524
}
35✔
1525

1526
void MerginApi::pingMergin()
24✔
1527
{
1528
  if ( mApiVersionStatus == MerginApiStatus::OK ) return;
24✔
1529

1530
  setApiVersionStatus( MerginApiStatus::PENDING );
24✔
1531

1532
  QNetworkRequest request = getDefaultRequest( false );
24✔
1533
  QUrl url( mApiRoot + QStringLiteral( "/ping" ) );
48✔
1534
  request.setUrl( url );
24✔
1535

1536
  QNetworkReply *reply = mManager.get( request );
24✔
1537
  CoreUtils::log( "ping", QStringLiteral( "Requesting: " ) + url.toString() );
48✔
1538
  connect( reply, &QNetworkReply::finished, this, &MerginApi::pingMerginReplyFinished );
24✔
1539
}
24✔
1540

1541
void MerginApi::migrateProjectToMergin( const QString &projectName, const QString &projectNamespace )
3✔
1542
{
1543
  CoreUtils::log( "migrate project", projectName );
3✔
1544
  if ( projectNamespace.isEmpty() )
3✔
1545
  {
1546
    createProject( mUserAuth->username(), projectName );
3✔
1547
  }
1548
  else
1549
  {
1550
    createProject( projectNamespace, projectName );
×
1551
  }
1552
}
3✔
1553

1554
void MerginApi::detachProjectFromMergin( const QString &projectNamespace, const QString &projectName, bool informUser )
1✔
1555
{
1556
  // Remove mergin folder
1557
  QString projectFullName = getFullProjectName( projectNamespace, projectName );
2✔
1558
  LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName );
1✔
1559

1560
  if ( projectInfo.isValid() )
1✔
1561
  {
1562
    CoreUtils::removeDir( projectInfo.projectDir + "/.mergin" );
1✔
1563
  }
1564

1565
  // Update localProject
1566
  mLocalProjects.updateNamespace( projectInfo.projectDir, "" );
1✔
1567
  mLocalProjects.updateLocalVersion( projectInfo.projectDir, -1 );
1✔
1568

1569
  if ( informUser )
1✔
1570
    emit notify( tr( "Project detached from Mergin" ) );
1✔
1571

1572
  emit projectDetached( projectFullName );
1✔
1573
}
1✔
1574

1575
QString MerginApi::apiRoot() const
131✔
1576
{
1577
  return mApiRoot;
131✔
1578
}
1579

1580
void MerginApi::setApiRoot( const QString &apiRoot )
4✔
1581
{
1582
  QString newApiRoot;
4✔
1583
  if ( apiRoot.isEmpty() )
4✔
1584
  {
1585
    newApiRoot = defaultApiRoot();
×
1586
  }
1587
  else
1588
  {
1589
    newApiRoot = apiRoot;
4✔
1590
  }
1591

1592
  if ( newApiRoot != mApiRoot )
4✔
1593
  {
1594
    mApiRoot = newApiRoot;
2✔
1595

1596
    QSettings settings;
2✔
1597
    settings.setValue( QStringLiteral( "Input/apiRoot" ), mApiRoot );
4✔
1598

1599
    emit apiRootChanged();
2✔
1600
  }
2✔
1601
}
4✔
1602

1603
QString MerginApi::merginUserName() const
29✔
1604
{
1605
  return userAuth()->username();
29✔
1606
}
1607

1608
QList<MerginFile> MerginApi::getLocalProjectFiles( const QString &projectPath )
585✔
1609
{
1610
  QElapsedTimer timer;
585✔
1611
  timer.start();
585✔
1612

1613
  QList<MerginFile> merginFiles;
585✔
1614
  ProjectChecksumCache checksumCache( projectPath );
585✔
1615

1616
  QSet<QString> localFiles = listFiles( projectPath );
585✔
1617
  for ( QString p : localFiles )
2,244✔
1618
  {
1619
    MerginFile file;
1,659✔
1620
    file.checksum = checksumCache.get( p );
1,659✔
1621
    file.path = p;
1,659✔
1622
    QFileInfo info( projectPath + p );
1,659✔
1623
    file.size = info.size();
1,659✔
1624
    file.mtime = info.lastModified();
1,659✔
1625
    merginFiles.append( file );
1,659✔
1626
  }
1,659✔
1627

1628
  qint64 elapsed = timer.elapsed();
585✔
1629
  if ( elapsed > 100 )
585✔
1630
  {
1631
    CoreUtils::log( "Local File", QStringLiteral( "It took %1 ms to create MerginFiles for %2 local files for %3." ).arg( elapsed ).arg( localFiles.count() ).arg( projectPath ) );
×
1632
  }
1633
  return merginFiles;
1,170✔
1634
}
585✔
1635

1636
void MerginApi::listProjectsReplyFinished( QString requestId )
22✔
1637
{
1638
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
22✔
1639
  Q_ASSERT( r );
22✔
1640

1641
  int projectCount = -1;
22✔
1642
  int requestedPage = 1;
22✔
1643
  MerginProjectsList projectList;
22✔
1644

1645
  if ( r->error() == QNetworkReply::NoError )
22✔
1646
  {
1647
    QUrlQuery query( r->request().url().query() );
44✔
1648
    requestedPage = query.queryItemValue( "page" ).toInt();
22✔
1649

1650
    QByteArray data = r->readAll();
22✔
1651
    QJsonDocument doc = QJsonDocument::fromJson( data );
22✔
1652

1653
    if ( doc.isObject() )
22✔
1654
    {
1655
      projectCount = doc.object().value( "count" ).toInt();
22✔
1656
      projectList = parseProjectsFromJson( doc );
22✔
1657
    }
1658

1659
    CoreUtils::log( "list projects", QStringLiteral( "Success - got %1 projects" ).arg( projectList.count() ) );
22✔
1660
  }
22✔
1661
  else
1662
  {
1663
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
1664
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listProjects" ), r->errorString(), serverMsg );
×
1665
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: listProjects" ) );
×
1666
    CoreUtils::log( "list projects", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
1667

1668
    emit listProjectsFailed();
×
1669
  }
×
1670

1671
  r->deleteLater();
22✔
1672

1673
  emit listProjectsFinished( projectList, projectCount, requestedPage, requestId );
22✔
1674
}
22✔
1675

1676
void MerginApi::listProjectsByNameReplyFinished( QString requestId )
7✔
1677
{
1678
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
7✔
1679
  Q_ASSERT( r );
7✔
1680

1681
  MerginProjectsList projectList;
7✔
1682

1683
  if ( r->error() == QNetworkReply::NoError )
7✔
1684
  {
1685
    QByteArray data = r->readAll();
7✔
1686
    QJsonDocument json = QJsonDocument::fromJson( data );
7✔
1687
    projectList = parseProjectsFromJson( json );
7✔
1688
    CoreUtils::log( "list projects by name", QStringLiteral( "Success - got %1 projects" ).arg( projectList.count() ) );
7✔
1689
  }
7✔
1690
  else
1691
  {
1692
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
1693
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listProjectsByName" ), r->errorString(), serverMsg );
×
1694
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: listProjectsByName" ) );
×
1695
    CoreUtils::log( "list projects by name", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
1696

1697
    emit listProjectsFailed();
×
1698
  }
×
1699

1700
  r->deleteLater();
7✔
1701

1702
  emit listProjectsByNameFinished( projectList, requestId );
7✔
1703
}
7✔
1704

1705

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

1710
  QString dest = projectDir + "/" + filePath;
202✔
1711
  createPathIfNotExists( dest );
202✔
1712

1713
  QFile f( dest );
202✔
1714
  if ( !f.open( QIODevice::WriteOnly ) )
202✔
1715
  {
1716
    CoreUtils::log( "pull " + projectFullName, "Failed to open file for writing " + dest );
×
1717
    return;
×
1718
  }
1719

1720
  // assemble file from tmp files
1721
  for ( const auto &item : items )
343✔
1722
  {
1723
    QFile fTmp( tempDir + "/" + item.tempFileName );
282✔
1724
    if ( !fTmp.open( QIODevice::ReadOnly ) )
141✔
1725
    {
1726
      CoreUtils::log( "pull " + projectFullName, "Failed to open temp file for reading " + item.tempFileName );
×
1727
      return;
×
1728
    }
1729
    f.write( fTmp.readAll() );
141✔
1730
  }
141✔
1731

1732
  f.close();
202✔
1733

1734
  // if diffable, copy to .mergin dir so we have a basefile
1735
  if ( MerginApi::isFileDiffable( filePath ) )
202✔
1736
  {
1737
    QString basefile = projectDir + "/.mergin/" + filePath;
15✔
1738
    createPathIfNotExists( basefile );
15✔
1739

1740
    if ( !QFile::remove( basefile ) )
15✔
1741
    {
1742
      CoreUtils::log( "pull " + projectFullName, "failed to remove old basefile for: " + filePath );
15✔
1743
    }
1744
    if ( !QFile::copy( dest, basefile ) )
15✔
1745
    {
1746
      CoreUtils::log( "pull " + projectFullName, "failed to copy new basefile for: " + filePath );
×
1747
    }
1748
  }
15✔
1749
}
202✔
1750

1751

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

1756
  // update diffable files that have been modified on the server
1757
  // - if they were not modified locally, the server changes will be simply applied
1758
  // - if they were modified locally, local changes will be rebased on top of server changes
1759

1760
  QString src = tempDir + "/" + CoreUtils::uuidWithoutBraces( QUuid::createUuid() );
18✔
1761
  QString dest = projectDir + "/" + filePath;
9✔
1762
  QString basefile = projectDir + "/.mergin/" + filePath;
9✔
1763

1764
  LocalProject info = mLocalProjects.projectFromMerginName( projectFullName );
9✔
1765

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

1769
  createPathIfNotExists( src );
9✔
1770
  createPathIfNotExists( dest );
9✔
1771
  createPathIfNotExists( basefile );
9✔
1772

1773
  QStringList diffFiles;
9✔
1774
  for ( const auto &item : items )
19✔
1775
    diffFiles << tempDir + "/" + item.tempFileName;
10✔
1776

1777
  //
1778
  // let's first assemble server's file from our basefile + diffs
1779
  //
1780

1781
  if ( !QFile::copy( basefile, src ) )
9✔
1782
  {
1783
    CoreUtils::log( "pull " + projectFullName, "assemble server file fail: copying failed " + basefile + " to " + src );
×
1784

1785
    // TODO: this is a critical failure - we should abort pull
1786
  }
1787

1788
  if ( !GeodiffUtils::applyDiffs( src, diffFiles ) )
9✔
1789
  {
1790
    CoreUtils::log( "pull " + projectFullName, "server file assembly failed: " + filePath );
×
1791

1792
    // TODO: this is a critical failure - we should abort pull
1793
    // TODO: we could try to delete the basefile and re-download it from scratch on next sync
1794
  }
1795
  else
1796
  {
1797
    CoreUtils::log( "pull " + projectFullName, "server file assembly successful: " + filePath );
9✔
1798
  }
1799

1800
  //
1801
  // now we are ready for the update of our local file
1802
  //
1803
  bool hasConflicts = false;
9✔
1804

1805
  bool res = GeodiffUtils::rebase( basefile,
9✔
1806
                                   src,
1807
                                   dest,
1808
                                   conflictfile
1809
                                 );
1810
  if ( res )
9✔
1811
  {
1812
    CoreUtils::log( "pull " + projectFullName, "geodiff rebase successful: " + filePath );
8✔
1813
  }
1814
  else
1815
  {
1816
    CoreUtils::log( "pull " + projectFullName, "geodiff rebase failed! " + filePath );
1✔
1817

1818
    // not good... something went wrong in rebase - we need to save the local changes
1819
    // let's put them into a conflict file and use the server version
1820
    hasConflicts = true;
1✔
1821
    LocalProject info = mLocalProjects.projectFromMerginName( projectFullName );
1✔
1822
    QString newDest = CoreUtils::findUniquePath( CoreUtils::generateConflictedCopyFileName( dest, mUserAuth->username(), info.localVersion ) );
2✔
1823
    if ( !QFile::rename( dest, newDest ) )
1✔
1824
    {
1825
      CoreUtils::log( "pull " + projectFullName, "failed rename of conflicting file after failed geodiff rebase: " + filePath );
×
1826
    }
1827
    if ( !QFile::copy( src, dest ) )
1✔
1828
    {
1829
      CoreUtils::log( "pull " + projectFullName, "failed to update local conflicting file after failed geodiff rebase: " + filePath );
×
1830
    }
1831
  }
1✔
1832

1833
  //
1834
  // finally update our basefile
1835
  //
1836

1837
  if ( !QFile::remove( basefile ) )
9✔
1838
  {
1839
    CoreUtils::log( "pull " + projectFullName, "failed removal of old basefile: " + filePath );
×
1840

1841
    // TODO: this is a critical failure - we should abort pull
1842
  }
1843
  if ( !QFile::rename( src, basefile ) )
9✔
1844
  {
1845
    CoreUtils::log( "pull " + projectFullName, "failed rename of basefile using new server content: " + filePath );
×
1846

1847
    // TODO: this is a critical failure - we should abort pull
1848
  }
1849
  return hasConflicts;
9✔
1850
}
9✔
1851

1852
void MerginApi::finalizeProjectPull( const QString &projectFullName )
123✔
1853
{
1854
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
123✔
1855
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
123✔
1856

1857
  QString projectDir = transaction.projectDir;
123✔
1858
  QString tempProjectDir = getTempProjectDir( projectFullName );
123✔
1859

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

1862
  for ( const PullTask &finalizationItem : transaction.pullTasks )
337✔
1863
  {
1864
    switch ( finalizationItem.method )
214✔
1865
    {
1866
      case PullTask::Copy:
199✔
1867
      {
1868
        finalizeProjectPullCopy( projectFullName, projectDir, tempProjectDir, finalizationItem.filePath, finalizationItem.data );
199✔
1869
        break;
199✔
1870
      }
1871

1872
      case PullTask::CopyConflict:
3✔
1873
      {
1874
        // move local file to conflict file
1875
        QString origPath = projectDir + "/" + finalizationItem.filePath;
3✔
1876
        LocalProject info = mLocalProjects.projectFromMerginName( projectFullName );
3✔
1877
        QString newPath = CoreUtils::findUniquePath( CoreUtils::generateConflictedCopyFileName( origPath, mUserAuth->username(), info.localVersion ) );
6✔
1878
        if ( !QFile::rename( origPath, newPath ) )
3✔
1879
        {
1880
          CoreUtils::log( "pull " + projectFullName, "failed rename of conflicting file: " + finalizationItem.filePath );
×
1881
        }
1882
        else
1883
        {
1884
          CoreUtils::log( "pull " + projectFullName, "Local file renamed due to conflict with server: " + finalizationItem.filePath );
3✔
1885
        }
1886
        finalizeProjectPullCopy( projectFullName, projectDir, tempProjectDir, finalizationItem.filePath, finalizationItem.data );
3✔
1887
        break;
3✔
1888
      }
3✔
1889

1890
      case PullTask::ApplyDiff:
9✔
1891
      {
1892
        // applying diff can result in conflicted copy too, in this case
1893
        // we need to update gpkgSchemaChanged flag.
1894
        bool res = finalizeProjectPullApplyDiff( projectFullName, projectDir, tempProjectDir, finalizationItem.filePath, finalizationItem.data );
9✔
1895
        transaction.gpkgSchemaChanged = res;
9✔
1896
        break;
9✔
1897
      }
1898

1899
      case PullTask::Delete:
3✔
1900
      {
1901
        CoreUtils::log( "pull " + projectFullName, "Removing local file: " + finalizationItem.filePath );
3✔
1902
        QFile file( projectDir + "/" + finalizationItem.filePath );
6✔
1903
        file.remove();
3✔
1904
        break;
3✔
1905
      }
3✔
1906
    }
1907

1908
    // remove tmp files associated with this item
1909
    for ( const auto &downloadItem : finalizationItem.data )
365✔
1910
    {
1911
      if ( !QFile::remove( tempProjectDir + "/" + downloadItem.tempFileName ) )
151✔
1912
        CoreUtils::log( "pull " + projectFullName, "Failed to remove temporary file " + downloadItem.tempFileName );
×
1913
    }
1914
  }
1915

1916
  // check there are no files left
1917
  int tmpFilesLeft = QDir( tempProjectDir ).entryList( QDir::NoDotAndDotDot ).count();
123✔
1918
  if ( tmpFilesLeft )
123✔
1919
  {
1920
    CoreUtils::log( "pull " + projectFullName, "Some temporary files were left - this should not happen..." );
×
1921
  }
1922

1923
  QDir( tempProjectDir ).removeRecursively();
123✔
1924

1925
  // add the local project if not there yet
1926
  if ( !mLocalProjects.projectFromMerginName( projectFullName ).isValid() )
123✔
1927
  {
1928
    QString projectNamespace, projectName;
61✔
1929
    extractProjectName( projectFullName, projectNamespace, projectName );
61✔
1930

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

1935
    mLocalProjects.addMerginProject( projectDir, projectNamespace, projectName );
61✔
1936
  }
61✔
1937

1938
  finishProjectSync( projectFullName, true );
123✔
1939
}
123✔
1940

1941

1942
void MerginApi::pushStartReplyFinished()
115✔
1943
{
1944
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
115✔
1945
  Q_ASSERT( r );
115✔
1946

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

1949
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
115✔
1950
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
115✔
1951
  Q_ASSERT( r == transaction.replyPushStart );
115✔
1952

1953
  if ( r->error() == QNetworkReply::NoError )
115✔
1954
  {
1955
    QByteArray data = r->readAll();
115✔
1956

1957
    transaction.replyPushStart->deleteLater();
115✔
1958
    transaction.replyPushStart = nullptr;
115✔
1959

1960
    QList<MerginFile> files = transaction.pushQueue;
115✔
1961
    if ( !files.isEmpty() )
115✔
1962
    {
1963
      QString transactionUUID;
111✔
1964
      QJsonDocument doc = QJsonDocument::fromJson( data );
111✔
1965
      if ( doc.isObject() )
111✔
1966
      {
1967
        QJsonObject docObj = doc.object();
111✔
1968
        transactionUUID = docObj.value( QStringLiteral( "transaction" ) ).toString();
111✔
1969
        transaction.transactionUUID = transactionUUID;
111✔
1970
      }
111✔
1971

1972
      if ( transaction.transactionUUID.isEmpty() )
111✔
1973
      {
1974
        CoreUtils::log( "push " + projectFullName, QStringLiteral( "Fail! Could not acquire transaction ID" ) );
×
1975
        finishProjectSync( projectFullName, false );
×
1976
      }
1977

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

1980
      MerginFile file = files.first();
111✔
1981
      pushFile( projectFullName, transactionUUID, file );
111✔
1982
      emit pushFilesStarted();
111✔
1983
    }
111✔
1984
    else  // pushing only files to be removed
1985
    {
1986
      // we are done here - no upload of chunks, no request to "finish"
1987
      // because server immediatelly creates a new version without starting a transaction to upload chunks
1988

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

1991
      transaction.projectMetadata = data;
4✔
1992
      transaction.version = MerginProjectMetadata::fromJson( data ).version;
4✔
1993

1994
      finishProjectSync( projectFullName, true );
4✔
1995
    }
1996
  }
115✔
1997
  else
1998
  {
1999
    QByteArray data = r->readAll();
×
2000
    QString serverMsg = extractServerErrorMsg( data );
×
2001
    QString code = extractServerErrorCode( data );
×
2002
    bool showLimitReachedDialog = EnumHelper::isEqual( code, ErrorCode::StorageLimitHit );
×
2003

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

2006
    transaction.replyPushStart->deleteLater();
×
2007
    transaction.replyPushStart = nullptr;
×
2008

2009
    if ( showLimitReachedDialog )
×
2010
    {
2011
      const QList<MerginFile> files = transaction.pushQueue;
×
2012
      qreal uploadSize = 0;
×
2013
      for ( const MerginFile &f : files )
×
2014
      {
2015
        uploadSize += f.size;
×
2016
      }
2017
      emit storageLimitReached( uploadSize );
×
2018

2019
      // remove project if it was first time sync - migration
2020
      if ( transaction.isInitialPush )
×
2021
      {
2022
        QString projectNamespace, projectName;
×
2023
        extractProjectName( projectFullName, projectNamespace, projectName );
×
2024

2025
        detachProjectFromMergin( projectNamespace, projectName, false );
×
2026
        deleteProject( projectNamespace, projectName, false );
×
2027
      }
×
2028
    }
×
2029
    else
2030
    {
2031
      int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
×
2032
      emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: pushStartReply" ), httpCode, projectFullName );
×
2033
    }
2034
    finishProjectSync( projectFullName, false );
×
2035
  }
×
2036
}
115✔
2037

2038
void MerginApi::pushFileReplyFinished()
152✔
2039
{
2040
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
152✔
2041
  Q_ASSERT( r );
152✔
2042

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

2045
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
152✔
2046
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
152✔
2047
  Q_ASSERT( r == transaction.replyPushFile );
152✔
2048

2049
  QStringList params = ( r->url().toString().split( "/" ) );
304✔
2050
  QString transactionUUID = params.at( params.length() - 2 );
152✔
2051
  QString chunkID = params.at( params.length() - 1 );
152✔
2052
  Q_ASSERT( transactionUUID == transaction.transactionUUID );
152✔
2053

2054
  if ( r->error() == QNetworkReply::NoError )
152✔
2055
  {
2056
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Uploaded successfully: " ) + chunkID );
151✔
2057

2058
    transaction.replyPushFile->deleteLater();
151✔
2059
    transaction.replyPushFile = nullptr;
151✔
2060

2061
    MerginFile currentFile = transaction.pushQueue.first();
151✔
2062
    int chunkNo = currentFile.chunks.indexOf( chunkID );
151✔
2063
    if ( chunkNo < currentFile.chunks.size() - 1 )
151✔
2064
    {
2065
      pushFile( projectFullName, transactionUUID, currentFile, chunkNo + 1 );
2✔
2066
    }
2067
    else
2068
    {
2069
      transaction.transferedSize += currentFile.size;
149✔
2070

2071
      emit syncProjectStatusChanged( projectFullName, transaction.transferedSize / transaction.totalSize );
149✔
2072
      transaction.pushQueue.removeFirst();
149✔
2073

2074
      if ( !transaction.pushQueue.isEmpty() )
149✔
2075
      {
2076
        MerginFile nextFile = transaction.pushQueue.first();
39✔
2077
        pushFile( projectFullName, transactionUUID, nextFile );
39✔
2078
      }
39✔
2079
      else
2080
      {
2081
        pushFinish( projectFullName, transactionUUID );
110✔
2082
      }
2083
    }
2084
  }
151✔
2085
  else
2086
  {
2087
    QString serverMsg = extractServerErrorMsg( r->readAll() );
2✔
2088
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) );
1✔
2089

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

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

2096
    finishProjectSync( projectFullName, false );
1✔
2097
  }
1✔
2098
}
152✔
2099

2100
void MerginApi::pullInfoReplyFinished()
100✔
2101
{
2102
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
100✔
2103
  Q_ASSERT( r );
100✔
2104

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

2107
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
100✔
2108
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
100✔
2109
  Q_ASSERT( r == transaction.replyPullProjectInfo );
100✔
2110

2111
  if ( r->error() == QNetworkReply::NoError )
100✔
2112
  {
2113
    QByteArray data = r->readAll();
99✔
2114
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Downloaded project info." ) );
99✔
2115

2116
    transaction.replyPullProjectInfo->deleteLater();
99✔
2117
    transaction.replyPullProjectInfo = nullptr;
99✔
2118

2119
    prepareProjectPull( projectFullName, data );
99✔
2120
  }
99✔
2121
  else
2122
  {
2123
    QString serverMsg = extractServerErrorMsg( r->readAll() );
2✔
2124
    QString message = QStringLiteral( "Network API error: %1(): %2" ).arg( QStringLiteral( "projectInfo" ), r->errorString() );
3✔
2125
    CoreUtils::log( "pull " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) );
1✔
2126

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

2130
    transaction.replyPullProjectInfo->deleteLater();
1✔
2131
    transaction.replyPullProjectInfo = nullptr;
1✔
2132

2133
    finishProjectSync( projectFullName, false );
1✔
2134
  }
1✔
2135
}
100✔
2136

2137
void MerginApi::prepareProjectPull( const QString &projectFullName, const QByteArray &data )
125✔
2138
{
2139
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
125✔
2140
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
125✔
2141

2142
  MerginProjectMetadata serverProject = MerginProjectMetadata::fromJson( data );
125✔
2143

2144
  transaction.projectMetadata = data;
125✔
2145
  transaction.version = serverProject.version;
125✔
2146

2147
  LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName );
125✔
2148
  if ( projectInfo.isValid() )
125✔
2149
  {
2150
    transaction.projectDir = projectInfo.projectDir;
63✔
2151

2152
    // do not continue if we are already on the latest version
2153
    if ( projectInfo.localVersion != -1 && projectInfo.localVersion == serverProject.version )
63✔
2154
    {
2155
      emit projectAlreadyOnLatestVersion( projectFullName );
1✔
2156
      CoreUtils::log( QStringLiteral( "Pull %1" ).arg( projectFullName ), QStringLiteral( "Project is already on the latest version: %1" ).arg( serverProject.version ) );
2✔
2157

2158
      return finishProjectSync( projectFullName, false );
1✔
2159
    }
2160
  }
2161
  else
2162
  {
2163
    QString projectNamespace;
62✔
2164
    QString projectName;
62✔
2165
    extractProjectName( projectFullName, projectNamespace, projectName );
62✔
2166

2167
    // remove any leftover temp files that could be created from previous unsuccessful download
2168
    removeProjectsTempFolder( projectNamespace, projectName );
62✔
2169

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

2174
    // create file indicating first time download in progress
2175
    QString downloadInProgressFilePath = CoreUtils::downloadInProgressFilePath( transaction.projectDir );
62✔
2176
    createPathIfNotExists( downloadInProgressFilePath );
62✔
2177
    if ( !CoreUtils::createEmptyFile( downloadInProgressFilePath ) )
62✔
2178
      CoreUtils::log( QStringLiteral( "pull %1" ).arg( projectFullName ), "Unable to create temporary download in progress file" );
×
2179

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

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

2185
  if ( transaction.configAllowed )
124✔
2186
  {
2187
    prepareDownloadConfig( projectFullName );
112✔
2188
  }
2189
  else
2190
  {
2191
    startProjectPull( projectFullName );
12✔
2192
  }
2193
}
126✔
2194

2195
void MerginApi::startProjectPull( const QString &projectFullName )
124✔
2196
{
2197
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
124✔
2198
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
124✔
2199

2200
  QList<MerginFile> localFiles = getLocalProjectFiles( transaction.projectDir + "/" );
124✔
2201
  MerginProjectMetadata serverProject = MerginProjectMetadata::fromJson( transaction.projectMetadata );
124✔
2202
  MerginProjectMetadata oldServerProject = MerginProjectMetadata::fromCachedJson( transaction.projectDir + "/" + sMetadataFile );
248✔
2203
  MerginConfig oldTransactionConfig = MerginConfig::fromFile( transaction.projectDir + "/" + sMerginConfigFile );
248✔
2204

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

2208
  transaction.diff = compareProjectFiles(
124✔
2209
                       oldServerProject.files,
2210
                       serverProject.files,
2211
                       localFiles,
2212
                       transaction.projectDir,
124✔
2213
                       transaction.configAllowed,
124✔
2214
                       transaction.config,
124✔
2215
                       oldTransactionConfig );
124✔
2216

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

2219
  for ( QString filePath : transaction.diff.remoteAdded )
317✔
2220
  {
2221
    MerginFile file = serverProject.fileInfo( filePath );
193✔
2222
    QList<DownloadQueueItem> items = itemsForFileChunks( file, transaction.version );
193✔
2223
    transaction.pullTasks << PullTask( PullTask::Copy, filePath, items );
193✔
2224
    transaction.gpkgSchemaChanged = true;
193✔
2225
  }
193✔
2226

2227
  for ( QString filePath : transaction.diff.remoteUpdated )
138✔
2228
  {
2229
    MerginFile file = serverProject.fileInfo( filePath );
14✔
2230

2231
    // for diffable files - download and apply to the basefile (without rebase)
2232
    if ( isFileDiffable( filePath ) && file.pullCanUseDiff )
14✔
2233
    {
2234
      QList<DownloadQueueItem> items = itemsForFileDiffs( file );
6✔
2235
      transaction.pullTasks << PullTask( PullTask::ApplyDiff, filePath, items );
6✔
2236
    }
6✔
2237
    else
2238
    {
2239
      QList<DownloadQueueItem> items = itemsForFileChunks( file, transaction.version );
8✔
2240
      transaction.pullTasks << PullTask( PullTask::Copy, filePath, items );
8✔
2241
      transaction.gpkgSchemaChanged = true;
8✔
2242
    }
8✔
2243
  }
14✔
2244

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

2250
    // for diffable files - download and apply to the basefile (will also do rebase)
2251
    if ( isFileDiffable( filePath ) && file.pullCanUseDiff )
5✔
2252
    {
2253
      QList<DownloadQueueItem> items = itemsForFileDiffs( file );
3✔
2254
      transaction.pullTasks << PullTask( PullTask::ApplyDiff, filePath, items );
3✔
2255
    }
3✔
2256
    else
2257
    {
2258
      QList<DownloadQueueItem> items = itemsForFileChunks( file, transaction.version );
2✔
2259
      transaction.pullTasks << PullTask( PullTask::CopyConflict, filePath, items );
2✔
2260
      transaction.gpkgSchemaChanged = true;
2✔
2261
    }
2✔
2262
  }
5✔
2263

2264
  // also download files which were added both on the server and locally (the local version will be renamed as conflicting copy)
2265
  for ( QString filePath : transaction.diff.conflictRemoteAddedLocalAdded )
125✔
2266
  {
2267
    MerginFile file = serverProject.fileInfo( filePath );
1✔
2268
    QList<DownloadQueueItem> items = itemsForFileChunks( file, transaction.version );
1✔
2269
    transaction.pullTasks << PullTask( PullTask::CopyConflict, filePath, items );
1✔
2270
    transaction.gpkgSchemaChanged = true;
1✔
2271
  }
1✔
2272

2273
  // schedule removed files to be deleted
2274
  for ( QString filePath : transaction.diff.remoteDeleted )
127✔
2275
  {
2276
    transaction.pullTasks << PullTask( PullTask::Delete, filePath, QList<DownloadQueueItem>() );
3✔
2277
  }
3✔
2278

2279
  // prepare the download queue
2280
  for ( const PullTask &item : transaction.pullTasks )
340✔
2281
  {
2282
    transaction.downloadQueue << item.data;
216✔
2283
  }
2284

2285
  qint64 totalSize = 0;
124✔
2286
  for ( const DownloadQueueItem &item : transaction.downloadQueue )
277✔
2287
  {
2288
    totalSize += item.size;
153✔
2289
  }
2290
  transaction.totalSize = totalSize;
124✔
2291

2292
  CoreUtils::log( "pull " + projectFullName, QStringLiteral( "%1 update tasks, %2 items to download (total size %3 bytes)" )
372✔
2293
                  .arg( transaction.pullTasks.count() )
248✔
2294
                  .arg( transaction.downloadQueue.count() )
248✔
2295
                  .arg( transaction.totalSize ) );
248✔
2296

2297
  emit pullFilesStarted();
124✔
2298
  downloadNextItem( projectFullName );
124✔
2299
}
124✔
2300

2301
void MerginApi::prepareDownloadConfig( const QString &projectFullName, bool downloaded )
151✔
2302
{
2303
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
151✔
2304
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
151✔
2305

2306
  MerginProjectMetadata newServerVersion = MerginProjectMetadata::fromJson( transaction.projectMetadata );
151✔
2307

2308
  const auto res = std::find_if( newServerVersion.files.begin(), newServerVersion.files.end(), []( const MerginFile & file )
151✔
2309
  {
2310
    return file.path == sMerginConfigFile;
523✔
2311
  } );
2312
  bool serverContainsConfig = res != newServerVersion.files.end();
151✔
2313

2314
  if ( serverContainsConfig )
151✔
2315
  {
2316
    if ( !downloaded )
78✔
2317
    {
2318
      // we should have server config but we do not have it yet
2319
      return requestServerConfig( projectFullName );
39✔
2320
    }
2321
  }
2322

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

2325
  const auto resOld = std::find_if( oldServerVersion.files.begin(), oldServerVersion.files.end(), []( const MerginFile & file )
112✔
2326
  {
2327
    return file.path == sMerginConfigFile;
203✔
2328
  } );
2329

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

2332
  if ( !transaction.config.isValid )
112✔
2333
  {
2334
    // if transaction is not valid (or missing), consider it as deleted
2335
    transaction.config.downloadMissingFiles = true;
74✔
2336
    CoreUtils::log( "MerginConfig", "No config detected" );
74✔
2337
  }
2338
  else if ( serverContainsConfig && previousVersionContainedConfig )
38✔
2339
  {
2340
    // config was there, check if there are changes
2341
    QString newChk = newServerVersion.fileInfo( sMerginConfigFile ).checksum;
29✔
2342
    QString oldChk = oldServerVersion.fileInfo( sMerginConfigFile ).checksum;
29✔
2343

2344
    if ( newChk == oldChk )
29✔
2345
    {
2346
      // config files are the same
2347
    }
2348
    else
2349
    {
2350
      // config was changed, but what changed?
2351
      MerginConfig oldConfig = MerginConfig::fromFile( transaction.projectDir + "/" + MerginApi::sMerginConfigFile );
16✔
2352

2353
      if ( oldConfig.selectiveSyncEnabled != transaction.config.selectiveSyncEnabled )
8✔
2354
      {
2355
        // selective sync was enabled/disabled
2356
        if ( transaction.config.selectiveSyncEnabled )
4✔
2357
        {
2358
          CoreUtils::log( "MerginConfig", "Selective sync has been enabled" );
2✔
2359
        }
2360
        else
2361
        {
2362
          CoreUtils::log( "MerginConfig", "Selective sync has been disabled, downloading missing files." );
2✔
2363
          transaction.config.downloadMissingFiles = true;
2✔
2364
        }
2365
      }
2366
      else if ( oldConfig.selectiveSyncDir != transaction.config.selectiveSyncDir )
4✔
2367
      {
2368
        CoreUtils::log( "MerginConfig", "Selective sync directory has changed, downloading missing files." );
4✔
2369
        transaction.config.downloadMissingFiles = true;
4✔
2370
      }
2371
      else
2372
      {
2373
        CoreUtils::log( "MerginConfig", "Unknown change in config file, continuing with latest version." );
×
2374
      }
2375
    }
8✔
2376
  }
29✔
2377
  else if ( serverContainsConfig )
9✔
2378
  {
2379
    CoreUtils::log( "MerginConfig", "Detected new config file." );
9✔
2380
  }
2381
  else if ( previousVersionContainedConfig ) // and current does not
×
2382
  {
2383
    CoreUtils::log( "MerginConfig", "Config file was removed, downloading missing files." );
×
2384
    transaction.config.downloadMissingFiles = true;
×
2385
  }
2386
  else // no config in last versions
2387
  {
2388
    // pull like without config
2389
    transaction.configAllowed = false;
×
2390
    transaction.config.isValid = false;
×
2391

2392
    // if it would be possible to add mergin-config locally, it needs to be checked here
2393
  }
2394

2395
  startProjectPull( projectFullName );
112✔
2396
}
151✔
2397

2398
void MerginApi::requestServerConfig( const QString &projectFullName )
39✔
2399
{
2400
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
39✔
2401
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
39✔
2402

2403
  QUrl url( mApiRoot + QStringLiteral( "/v1/project/raw/" ) + projectFullName );
78✔
2404
  QUrlQuery query;
39✔
2405

2406
  query.addQueryItem( "file", sMerginConfigFile.toUtf8().toPercentEncoding() );
39✔
2407
  query.addQueryItem( "version", QStringLiteral( "v%1" ).arg( transaction.version ) );
39✔
2408
  url.setQuery( query );
39✔
2409

2410
  QNetworkRequest request = getDefaultRequest();
39✔
2411
  request.setUrl( url );
39✔
2412
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
39✔
2413

2414
  Q_ASSERT( !transaction.replyPullItem );
39✔
2415
  transaction.replyPullItem = mManager.get( request );
39✔
2416
  connect( transaction.replyPullItem, &QNetworkReply::finished, this, &MerginApi::cacheServerConfig );
39✔
2417

2418
  CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Requesting mergin config: " ) + url.toString() );
78✔
2419
}
39✔
2420

2421
QList<DownloadQueueItem> MerginApi::itemsForFileChunks( const MerginFile &file, int version )
204✔
2422
{
2423
  QList<DownloadQueueItem> lst;
204✔
2424
  int from = 0;
204✔
2425
  while ( from < file.size )
347✔
2426
  {
2427
    int size = qMin( MerginApi::UPLOAD_CHUNK_SIZE, static_cast<int>( file.size ) - from );
143✔
2428
    lst << DownloadQueueItem( file.path, size, version, from, from + size - 1 );
143✔
2429
    from += size;
143✔
2430
  }
2431
  return lst;
204✔
2432
}
×
2433

2434
QList<DownloadQueueItem> MerginApi::itemsForFileDiffs( const MerginFile &file )
9✔
2435
{
2436
  QList<DownloadQueueItem> items;
9✔
2437
  // download diffs instead of full download of gpkg file from server
2438
  for ( const auto &d : file.pullDiffFiles )
19✔
2439
  {
2440
    items << DownloadQueueItem( file.path, d.second, d.first, -1, -1, true );
10✔
2441
  }
2442
  return items;
9✔
2443
}
×
2444

2445

2446
static MerginFile findFile( const QString &filePath, const QList<MerginFile> &files )
155✔
2447
{
2448
  for ( const MerginFile &merginFile : files )
310✔
2449
  {
2450
    if ( merginFile.path == filePath )
310✔
2451
      return merginFile;
155✔
2452
  }
2453
  CoreUtils::log( QStringLiteral( "MerginFile" ), QStringLiteral( "requested findFile() for non-existant file: %1" ).arg( filePath ) );
×
2454
  return MerginFile();
×
2455
}
2456

2457

2458
void MerginApi::pushInfoReplyFinished()
143✔
2459
{
2460
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
143✔
2461
  Q_ASSERT( r );
143✔
2462

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

2465
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
143✔
2466
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
143✔
2467
  Q_ASSERT( r == transaction.replyPushProjectInfo );
143✔
2468

2469
  if ( r->error() == QNetworkReply::NoError )
143✔
2470
  {
2471
    QString url = r->url().toString();
142✔
2472
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Downloaded project info." ) );
142✔
2473
    QByteArray data = r->readAll();
142✔
2474

2475
    transaction.replyPushProjectInfo->deleteLater();
142✔
2476
    transaction.replyPushProjectInfo = nullptr;
142✔
2477

2478
    LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName );
142✔
2479
    transaction.projectDir = projectInfo.projectDir;
142✔
2480
    Q_ASSERT( !transaction.projectDir.isEmpty() );
142✔
2481

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

2485
    // now let's figure a key question: are we on the most recent version of the project
2486
    // if we're about to do upload? because if not, we need to do pull first
2487
    if ( projectInfo.isValid() && projectInfo.localVersion != -1 && projectInfo.localVersion < serverProject.version )
142✔
2488
    {
2489
      CoreUtils::log( "push " + projectFullName, QStringLiteral( "Need pull first: local version %1 | server version %2" )
78✔
2490
                      .arg( projectInfo.localVersion ).arg( serverProject.version ) );
52✔
2491
      transaction.pullBeforePush = true;
26✔
2492
      prepareProjectPull( projectFullName, data );
26✔
2493
      return;
26✔
2494
    }
2495

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

2499
    // Cache mergin-config, since we are on the most recent version, it is sufficient to just read the local version
2500
    if ( transaction.configAllowed )
116✔
2501
    {
2502
      transaction.config = MerginConfig::fromFile( transaction.projectDir + "/" + MerginApi::sMerginConfigFile );
107✔
2503
    }
2504

2505
    transaction.diff = compareProjectFiles(
232✔
2506
                         oldServerProject.files,
2507
                         serverProject.files,
2508
                         localFiles,
2509
                         transaction.projectDir,
116✔
2510
                         transaction.configAllowed,
116✔
2511
                         transaction.config
116✔
2512
                       );
116✔
2513

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

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

2518
    QList<MerginFile> filesToUpload;
116✔
2519
    QList<MerginFile> addedMerginFiles, updatedMerginFiles, deletedMerginFiles;
116✔
2520
    QList<MerginFile> diffFiles;
116✔
2521
    for ( QString filePath : transaction.diff.localAdded )
248✔
2522
    {
2523
      MerginFile merginFile = findFile( filePath, localFiles );
132✔
2524
      merginFile.chunks = generateChunkIdsForSize( merginFile.size );
132✔
2525
      addedMerginFiles.append( merginFile );
132✔
2526
    }
132✔
2527

2528
    for ( QString filePath : transaction.diff.localUpdated )
135✔
2529
    {
2530
      MerginFile merginFile = findFile( filePath, localFiles );
19✔
2531
      merginFile.chunks = generateChunkIdsForSize( merginFile.size );
19✔
2532

2533
      if ( MerginApi::isFileDiffable( filePath ) )
19✔
2534
      {
2535
        // try to create a diff
2536
        QString diffName;
12✔
2537
        int geodiffRes = GeodiffUtils::createChangeset( transaction.projectDir, filePath, diffName );
12✔
2538
        QString diffPath = transaction.projectDir + "/.mergin/" + diffName;
12✔
2539
        QString basePath = transaction.projectDir + "/.mergin/" + filePath;
12✔
2540

2541
        if ( geodiffRes == GEODIFF_SUCCESS )
12✔
2542
        {
2543
          QByteArray checksumDiff = CoreUtils::calculateChecksum( diffPath );
12✔
2544

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

2549
          merginFile.diffName = diffName;
12✔
2550
          merginFile.diffChecksum = QString::fromLatin1( checksumDiff.data(), checksumDiff.size() );
12✔
2551
          merginFile.diffSize = QFileInfo( diffPath ).size();
12✔
2552
          merginFile.chunks = generateChunkIdsForSize( merginFile.diffSize );
12✔
2553
          merginFile.diffBaseChecksum = QString::fromLatin1( checksumBase.data(), checksumBase.size() );
12✔
2554

2555
          diffFiles.append( merginFile );
12✔
2556

2557
          CoreUtils::log( "push " + projectFullName, QString( "Geodiff create changeset on %1 successful: total size %2 bytes" ).arg( filePath ).arg( merginFile.diffSize ) );
12✔
2558
        }
12✔
2559
        else
2560
        {
2561
          // TODO: remove the diff file (if exists)
2562
          CoreUtils::log( "push " + projectFullName, QString( "Geodiff create changeset on %1 FAILED with error %2 (will do full upload)" ).arg( filePath ).arg( geodiffRes ) );
×
2563
        }
2564
      }
12✔
2565

2566
      updatedMerginFiles.append( merginFile );
19✔
2567
    }
19✔
2568

2569
    for ( QString filePath : transaction.diff.localDeleted )
120✔
2570
    {
2571
      MerginFile merginFile = findFile( filePath, serverProject.files );
4✔
2572
      deletedMerginFiles.append( merginFile );
4✔
2573
    }
4✔
2574

2575
    if ( addedMerginFiles.isEmpty() && updatedMerginFiles.isEmpty() && deletedMerginFiles.isEmpty() )
116✔
2576
    {
2577
      // if nothing has changed, there is no point to even start upload transaction
2578
      transaction.projectMetadata = data;
1✔
2579
      transaction.version = MerginProjectMetadata::fromJson( data ).version;
1✔
2580

2581
      finishProjectSync( projectFullName, true );
1✔
2582
      return;
1✔
2583
    }
2584

2585
    QJsonArray added = prepareUploadChangesJSON( addedMerginFiles );
115✔
2586
    filesToUpload.append( addedMerginFiles );
115✔
2587

2588
    QJsonArray modified = prepareUploadChangesJSON( updatedMerginFiles );
115✔
2589
    filesToUpload.append( updatedMerginFiles );
115✔
2590

2591
    QJsonArray removed = prepareUploadChangesJSON( deletedMerginFiles );
115✔
2592
    // removed not in filesToUpload
2593

2594
    QJsonObject changes;
115✔
2595
    changes.insert( "added", added );
115✔
2596
    changes.insert( "removed", removed );
115✔
2597
    changes.insert( "updated", modified );
115✔
2598
    changes.insert( "renamed", QJsonArray() );
115✔
2599

2600
    qint64 totalSize = 0;
115✔
2601
    for ( MerginFile file : filesToUpload )
266✔
2602
    {
2603
      if ( !file.diffName.isEmpty() )
151✔
2604
        totalSize += file.diffSize;
12✔
2605
      else
2606
        totalSize += file.size;
139✔
2607
    }
151✔
2608

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

2612
    transaction.totalSize = totalSize;
115✔
2613
    transaction.pushQueue = filesToUpload;
115✔
2614
    transaction.pushDiffFiles = diffFiles;
115✔
2615

2616
    QJsonObject json;
115✔
2617
    json.insert( QStringLiteral( "changes" ), changes );
230✔
2618
    json.insert( QStringLiteral( "version" ), QString( "v%1" ).arg( serverProject.version ) );
230✔
2619
    QJsonDocument jsonDoc;
115✔
2620
    jsonDoc.setObject( json );
115✔
2621

2622
    pushStart( projectFullName, jsonDoc.toJson( QJsonDocument::Compact ) );
115✔
2623
  }
230✔
2624
  else
2625
  {
2626
    QString serverMsg = extractServerErrorMsg( r->readAll() );
2✔
2627
    QString message = QStringLiteral( "Network API error: %1(): %2" ).arg( QStringLiteral( "projectInfo" ), r->errorString() );
3✔
2628
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) );
1✔
2629

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

2633
    transaction.replyPushProjectInfo->deleteLater();
1✔
2634
    transaction.replyPushProjectInfo = nullptr;
1✔
2635

2636
    finishProjectSync( projectFullName, false );
1✔
2637
  }
1✔
2638
}
143✔
2639

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

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

2647
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
110✔
2648
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
110✔
2649
  Q_ASSERT( r == transaction.replyPushFinish );
110✔
2650

2651
  if ( r->error() == QNetworkReply::NoError )
110✔
2652
  {
2653
    Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
110✔
2654
    QByteArray data = r->readAll();
110✔
2655
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Transaction finish accepted" ) );
110✔
2656

2657
    transaction.replyPushFinish->deleteLater();
110✔
2658
    transaction.replyPushFinish = nullptr;
110✔
2659

2660
    transaction.projectMetadata = data;
110✔
2661
    transaction.version = MerginProjectMetadata::fromJson( data ).version;
110✔
2662

2663
    //  a new diffable files suppose to have their basefile copies in .mergin
2664
    for ( QString filePath : transaction.diff.localAdded )
240✔
2665
    {
2666
      if ( MerginApi::isFileDiffable( filePath ) )
130✔
2667
      {
2668
        QString basefile = transaction.projectDir + "/.mergin/" + filePath;
11✔
2669
        createPathIfNotExists( basefile );
11✔
2670

2671
        QString sourcePath = transaction.projectDir + "/" + filePath;
11✔
2672
        if ( !QFile::copy( sourcePath, basefile ) )
11✔
2673
        {
2674
          CoreUtils::log( "push " + projectFullName, "failed to copy new basefile for: " + filePath );
×
2675
        }
2676
      }
11✔
2677
    }
130✔
2678

2679
    // clean up diff-related files
2680
    const auto diffFiles = transaction.pushDiffFiles;
110✔
2681
    for ( const MerginFile &merginFile : diffFiles )
122✔
2682
    {
2683
      QString diffPath = transaction.projectDir + "/.mergin/" + merginFile.diffName;
12✔
2684

2685
      // update basefile (unmodified file that should be equivalent to the server)
2686
      QString basePath = transaction.projectDir + "/.mergin/" + merginFile.path;
12✔
2687
      bool res = GeodiffUtils::applyChangeset( basePath, diffPath );
12✔
2688
      if ( res )
12✔
2689
      {
2690
        CoreUtils::log( "push " + projectFullName, QString( "Applied %1 to base file of %2" ).arg( merginFile.diffName, merginFile.path ) );
12✔
2691
      }
2692
      else
2693
      {
2694
        CoreUtils::log( "push " + projectFullName, QString( "Failed to apply changeset %1 to basefile %2 - error %3" ).arg( diffPath ).arg( basePath ).arg( res ) );
×
2695
      }
2696

2697
      // remove temporary diff files
2698
      if ( !QFile::remove( diffPath ) )
12✔
2699
        CoreUtils::log( "push " + projectFullName, "Failed to remove diff: " + diffPath );
×
2700
    }
12✔
2701

2702
    finishProjectSync( projectFullName, true );
110✔
2703
  }
110✔
2704
  else
2705
  {
2706
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
2707
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "pushFinish" ), r->errorString(), serverMsg );
×
2708
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) );
×
2709

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

2713
    // remove temporary diff files
2714
    const auto diffFiles = transaction.pushDiffFiles;
×
2715
    for ( const MerginFile &merginFile : diffFiles )
×
2716
    {
2717
      QString diffPath = transaction.projectDir + "/.mergin/" + merginFile.diffName;
×
2718
      if ( !QFile::remove( diffPath ) )
×
2719
        CoreUtils::log( "push " + projectFullName, "Failed to remove diff: " + diffPath );
×
2720
    }
×
2721

2722
    transaction.replyPushFinish->deleteLater();
×
2723
    transaction.replyPushFinish = nullptr;
×
2724

2725
    finishProjectSync( projectFullName, false );
×
2726
  }
×
2727
}
110✔
2728

2729
void MerginApi::pushCancelReplyFinished()
1✔
2730
{
2731
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
1✔
2732
  Q_ASSERT( r );
1✔
2733

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

2736
  if ( r->error() == QNetworkReply::NoError )
1✔
2737
  {
2738
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "Transaction canceled" ) );
1✔
2739
  }
2740
  else
2741
  {
2742
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
2743
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "uploadCancel" ), r->errorString(), serverMsg );
×
2744
    CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) );
×
2745
  }
×
2746

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

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

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

2757
  if ( r->error() == QNetworkReply::NoError )
20✔
2758
  {
2759
    CoreUtils::log( "user info", QStringLiteral( "Success" ) );
19✔
2760
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
19✔
2761
    if ( doc.isObject() )
19✔
2762
    {
2763
      QJsonObject docObj = doc.object();
19✔
2764
      mUserInfo->setFromJson( docObj );
19✔
2765
      if ( mServerType == MerginServerType::OLD )
19✔
2766
      {
2767
        mWorkspaceInfo->setFromJson( docObj );
×
2768
      }
2769
    }
19✔
2770
  }
19✔
2771
  else
2772
  {
2773
    QString serverMsg = extractServerErrorMsg( r->readAll() );
2✔
2774
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "getUserInfo" ), r->errorString(), serverMsg );
3✔
2775
    CoreUtils::log( "user info", QStringLiteral( "FAILED - %1" ).arg( message ) );
1✔
2776
    mUserInfo->clear();
1✔
2777
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: getUserInfo" ) );
2✔
2778
  }
1✔
2779

2780
  emit userInfoReplyFinished();
20✔
2781

2782
  r->deleteLater();
20✔
2783
}
20✔
2784

2785
void MerginApi::getWorkspaceInfoReplyFinished()
25✔
2786
{
2787
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
25✔
2788
  Q_ASSERT( r );
25✔
2789

2790
  if ( r->error() == QNetworkReply::NoError )
25✔
2791
  {
2792
    CoreUtils::log( "workspace info", QStringLiteral( "Success" ) );
25✔
2793
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
25✔
2794
    if ( doc.isObject() )
25✔
2795
    {
2796
      QJsonObject docObj = doc.object();
25✔
2797
      mWorkspaceInfo->setFromJson( docObj );
25✔
2798

2799
      emit getWorkspaceInfoFinished();
25✔
2800
    }
25✔
2801
  }
25✔
2802
  else
2803
  {
2804
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
2805
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "getWorkspaceInfo" ), r->errorString(), serverMsg );
×
2806
    CoreUtils::log( "workspace info", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
2807
    mWorkspaceInfo->clear();
×
2808
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: getWorkspaceInfo" ) );
×
2809
  }
×
2810

2811
  r->deleteLater();
25✔
2812
}
25✔
2813

2814
bool MerginApi::hasLocalChanges(
328✔
2815
  const QList<MerginFile> &oldServerFiles,
2816
  const QList<MerginFile> &localFiles,
2817
  const QString &projectDir
2818
)
2819
{
2820
  if ( localFiles.count() != oldServerFiles.count() )
328✔
2821
  {
2822
    return true;
24✔
2823
  }
2824

2825
  QHash<QString, MerginFile> oldServerFilesMap;
304✔
2826

2827
  for ( const MerginFile &file : oldServerFiles )
970✔
2828
  {
2829
    oldServerFilesMap.insert( file.path, file );
666✔
2830
  }
2831

2832
  for ( const MerginFile &localFile : localFiles )
956✔
2833
  {
2834
    QString filePath = localFile.path;
665✔
2835
    bool hasOldServer = oldServerFilesMap.contains( localFile.path );
665✔
2836

2837
    if ( !hasOldServer )
665✔
2838
    {
2839
      // L-A
2840
      return true;
×
2841
    }
2842
    else
2843
    {
2844
      const QString chkOld = oldServerFilesMap.value( localFile.path ).checksum;
665✔
2845
      const QString chkLocal = localFile.checksum;
665✔
2846

2847
      if ( chkOld != chkLocal )
665✔
2848
      {
2849
        if ( isFileDiffable( filePath ) )
67✔
2850
        {
2851
          // we need to do a diff here to figure out whether the file is actually changed or not
2852
          // because the real content may be the same although the checksums do not match
2853
          // e.g. when GPKG is opened, its header is updated and therefore lastModified timestamp/checksum is updated as well.
2854
          if ( GeodiffUtils::hasPendingChanges( projectDir, filePath ) )
66✔
2855
          {
2856
            // L-U
2857
            return true;
12✔
2858
          }
2859
        }
2860
        else
2861
        {
2862
          // L-U
2863
          return true;
1✔
2864
        }
2865
      }
2866
    }
678✔
2867
  }
665✔
2868

2869
  // We know that the number of local files and old server is the same
2870
  // And also that all local files has old file counterpart
2871
  // So it is not possible that there is deleted local file at this point.
2872
  return false;
291✔
2873
}
304✔
2874

2875
ProjectDiff MerginApi::compareProjectFiles(
257✔
2876
  const QList<MerginFile> &oldServerFiles,
2877
  const QList<MerginFile> &newServerFiles,
2878
  const QList<MerginFile> &localFiles,
2879
  const QString &projectDir,
2880
  bool allowConfig,
2881
  const MerginConfig &config,
2882
  const MerginConfig &lastSyncConfig
2883
)
2884
{
2885
  ProjectDiff diff;
257✔
2886
  QHash<QString, MerginFile> oldServerFilesMap, newServerFilesMap;
257✔
2887

2888
  for ( MerginFile file : newServerFiles )
1,384✔
2889
  {
2890
    newServerFilesMap.insert( file.path, file );
1,127✔
2891
  }
1,127✔
2892
  for ( MerginFile file : oldServerFiles )
1,178✔
2893
  {
2894
    oldServerFilesMap.insert( file.path, file );
921✔
2895
  }
921✔
2896

2897
  for ( MerginFile localFile : localFiles )
1,164✔
2898
  {
2899
    QString filePath = localFile.path;
907✔
2900
    bool hasOldServer = oldServerFilesMap.contains( localFile.path );
907✔
2901
    bool hasNewServer = newServerFilesMap.contains( localFile.path );
907✔
2902
    QString chkOld = oldServerFilesMap.value( localFile.path ).checksum;
907✔
2903
    QString chkNew = newServerFilesMap.value( localFile.path ).checksum;
907✔
2904
    QString chkLocal = localFile.checksum;
907✔
2905

2906
    if ( !hasOldServer && !hasNewServer )
907✔
2907
    {
2908
      // L-A
2909
      diff.localAdded << filePath;
159✔
2910
    }
2911
    else if ( hasOldServer && !hasNewServer )
748✔
2912
    {
2913
      if ( chkOld == chkLocal )
4✔
2914
      {
2915
        // R-D
2916
        diff.remoteDeleted << filePath;
3✔
2917
      }
2918
      else
2919
      {
2920
        // C/R-D/L-U
2921
        diff.conflictRemoteDeletedLocalUpdated << filePath;
1✔
2922
      }
2923
    }
2924
    else if ( !hasOldServer && hasNewServer )
744✔
2925
    {
2926
      if ( chkNew != chkLocal )
1✔
2927
      {
2928
        // C/R-A/L-A
2929
        diff.conflictRemoteAddedLocalAdded << filePath;
1✔
2930
      }
2931
      else
2932
      {
2933
        // R-A/L-A
2934
        // TODO: need to do anything?
2935
      }
2936
    }
2937
    else if ( hasOldServer && hasNewServer )
743✔
2938
    {
2939
      // file has already existed
2940
      if ( chkOld == chkNew )
743✔
2941
      {
2942
        if ( chkNew != chkLocal )
724✔
2943
        {
2944
          // L-U
2945
          if ( isFileDiffable( filePath ) )
29✔
2946
          {
2947
            // we need to do a diff here to figure out whether the file is actually changed or not
2948
            // because the real content may be the same although the checksums do not match
2949
            if ( GeodiffUtils::hasPendingChanges( projectDir, filePath ) )
22✔
2950
              diff.localUpdated << filePath;
17✔
2951
          }
2952
          else
2953
            diff.localUpdated << filePath;
7✔
2954
        }
2955
        else
2956
        {
2957
          // no change :-)
2958
        }
2959
      }
2960
      else   // v1 != v2
2961
      {
2962
        if ( chkNew != chkLocal && chkOld != chkLocal )
19✔
2963
        {
2964
          // C/R-U/L-U
2965
          if ( isFileDiffable( filePath ) )
7✔
2966
          {
2967
            // we need to do a diff here to figure out whether the file is actually changed or not
2968
            // because the real content may be the same although the checksums do not match
2969
            if ( GeodiffUtils::hasPendingChanges( projectDir, filePath ) )
5✔
2970
              diff.conflictRemoteUpdatedLocalUpdated << filePath;
3✔
2971
            else
2972
              diff.remoteUpdated << filePath;
2✔
2973
          }
2974
          else
2975
            diff.conflictRemoteUpdatedLocalUpdated << filePath;
2✔
2976
        }
2977
        else if ( chkNew != chkLocal )  // && old == local
12✔
2978
        {
2979
          // R-U
2980
          diff.remoteUpdated << filePath;
12✔
2981
        }
2982
        else if ( chkOld != chkLocal )  // && new == local
×
2983
        {
2984
          // R-U/L-U
2985
          // TODO: need to do anything?
2986
        }
2987
        else
2988
          Q_ASSERT( false );   // impossible - should be handled already
×
2989
      }
2990
    }
2991

2992
    if ( hasOldServer )
907✔
2993
      oldServerFilesMap.remove( filePath );
747✔
2994
    if ( hasNewServer )
907✔
2995
      newServerFilesMap.remove( filePath );
744✔
2996
  }
907✔
2997

2998
  // go through files listed on the server, but not available locally
2999
  for ( MerginFile file : newServerFilesMap )
640✔
3000
  {
3001
    bool hasOldServer = oldServerFilesMap.contains( file.path );
383✔
3002

3003
    if ( hasOldServer )
383✔
3004
    {
3005
      if ( oldServerFilesMap.value( file.path ).checksum == file.checksum )
174✔
3006
      {
3007
        // L-D
3008
        if ( allowConfig )
174✔
3009
        {
3010
          bool shouldBeExcludedFromSync = MerginApi::excludeFromSync( file.path, config );
173✔
3011
          if ( shouldBeExcludedFromSync )
173✔
3012
          {
3013
            continue;
151✔
3014
          }
3015

3016
          // check if we should download missing files that were previously ignored (e.g. selective sync has been disabled)
3017
          bool previouslyIgnoredButShouldDownload = \
3018
              config.downloadMissingFiles &&
41✔
3019
              lastSyncConfig.isValid &&
41✔
3020
              MerginApi::excludeFromSync( file.path, lastSyncConfig );
19✔
3021

3022
          if ( previouslyIgnoredButShouldDownload )
22✔
3023
          {
3024
            diff.remoteAdded << file.path;
19✔
3025
            continue;
19✔
3026
          }
3027
        }
3028
        diff.localDeleted << file.path;
4✔
3029
      }
3030
      else
3031
      {
3032
        // C/R-U/L-D
3033
        diff.conflictRemoteUpdatedLocalDeleted << file.path;
×
3034
      }
3035
    }
3036
    else
3037
    {
3038
      // R-A
3039
      if ( allowConfig )
209✔
3040
      {
3041
        if ( MerginApi::excludeFromSync( file.path, config ) )
167✔
3042
        {
3043
          continue;
35✔
3044
        }
3045
      }
3046
      diff.remoteAdded << file.path;
174✔
3047
    }
3048

3049
    if ( hasOldServer )
178✔
3050
      oldServerFilesMap.remove( file.path );
4✔
3051
  }
383✔
3052

3053
  /*
3054
  for ( MerginFile file : oldServerFilesMap )
3055
  {
3056
    // R-D/L-D
3057
    // TODO: need to do anything?
3058
  }
3059
  */
3060

3061
  return diff;
514✔
3062
}
257✔
3063

3064
MerginProject MerginApi::parseProjectMetadata( const QJsonObject &proj )
286✔
3065
{
3066
  MerginProject project;
286✔
3067

3068
  if ( proj.isEmpty() )
286✔
3069
  {
3070
    return project;
×
3071
  }
3072

3073
  if ( proj.contains( QStringLiteral( "error" ) ) )
286✔
3074
  {
3075
    // handle project error (user might be logged out / do not have write rights / project is on different server / project is orphaned)
3076
    project.remoteError = QString::number( proj.value( QStringLiteral( "error" ) ).toInt( 0 ) ); // error code
×
3077
    return project;
×
3078
  }
3079

3080
  project.projectName = proj.value( QStringLiteral( "name" ) ).toString();
286✔
3081
  project.projectNamespace = proj.value( QStringLiteral( "namespace" ) ).toString();
286✔
3082

3083
  QString versionStr = proj.value( QStringLiteral( "version" ) ).toString();
572✔
3084
  if ( versionStr.isEmpty() )
286✔
3085
  {
3086
    project.serverVersion = 0;
×
3087
  }
3088
  else if ( versionStr.startsWith( "v" ) ) // cut off 'v' part from v123
286✔
3089
  {
3090
    versionStr = versionStr.mid( 1 );
286✔
3091
    project.serverVersion = versionStr.toInt();
286✔
3092
  }
3093

3094
  QDateTime updated = QDateTime::fromString( proj.value( QStringLiteral( "updated" ) ).toString(), Qt::ISODateWithMs ).toUTC();
572✔
3095
  if ( !updated.isValid() )
286✔
3096
  {
3097
    project.serverUpdated = QDateTime::fromString( proj.value( QStringLiteral( "created" ) ).toString(), Qt::ISODateWithMs ).toUTC();
×
3098
  }
3099
  else
3100
  {
3101
    project.serverUpdated = updated;
286✔
3102
  }
3103
  return project;
286✔
3104
}
286✔
3105

3106

3107
MerginProjectsList MerginApi::parseProjectsFromJson( const QJsonDocument &doc )
29✔
3108
{
3109
  if ( !doc.isObject() )
29✔
3110
    return MerginProjectsList();
×
3111

3112
  QJsonObject object = doc.object();
29✔
3113
  MerginProjectsList result;
29✔
3114

3115
  if ( object.contains( "projects" ) && object.value( "projects" ).isArray() ) // listProjects API
29✔
3116
  {
3117
    QJsonArray vArray = object.value( "projects" ).toArray();
44✔
3118

3119
    for ( auto it = vArray.constBegin(); it != vArray.constEnd(); ++it )
195✔
3120
    {
3121
      result << parseProjectMetadata( it->toObject() );
173✔
3122
    }
3123
  }
22✔
3124
  else if ( !object.isEmpty() ) // listProjectsbyName API returns projects as separate objects not in array
7✔
3125
  {
3126
    for ( auto it = object.begin(); it != object.end(); ++it )
120✔
3127
    {
3128
      MerginProject project = parseProjectMetadata( it->toObject() );
113✔
3129
      if ( !project.remoteError.isEmpty() )
113✔
3130
      {
3131
        // add project namespace/name from object name in case of error
3132
        MerginApi::extractProjectName( it.key(), project.projectNamespace, project.projectName );
×
3133
      }
3134
      result << project;
113✔
3135
    }
113✔
3136
  }
3137
  return result;
29✔
3138
}
29✔
3139

3140
void MerginApi::refreshAuthToken()
7✔
3141
{
3142
  if ( !mUserAuth->hasAuthData() ||
14✔
3143
       mUserAuth->authToken().isEmpty() )
14✔
3144
  {
3145
    CoreUtils::log( QStringLiteral( "Auth" ), QStringLiteral( "Can not refresh token, missing credentials" ) );
×
3146
    return;
×
3147
  }
3148

3149
  if ( mUserAuth->tokenExpiration() < QDateTime::currentDateTimeUtc() )
7✔
3150
  {
3151
    CoreUtils::log( QStringLiteral( "Auth" ), QStringLiteral( "Token has expired, requesting new one" ) );
×
3152
    authorize( mUserAuth->username(), mUserAuth->password() );
×
3153
    mAuthLoopEvent.exec();
×
3154
  }
3155
}
3156

3157
QStringList MerginApi::generateChunkIdsForSize( qint64 fileSize )
163✔
3158
{
3159
  qreal rawNoOfChunks = qreal( fileSize ) / UPLOAD_CHUNK_SIZE;
163✔
3160
  int noOfChunks = qCeil( rawNoOfChunks );
163✔
3161

3162
  // edge case when file is empty, filesize equals zero
3163
  // manually set one chunk so that file will be synced
3164
  if ( fileSize <= 0 )
163✔
3165
    noOfChunks = 1;
45✔
3166

3167
  QStringList chunks;
163✔
3168
  for ( int i = 0; i < noOfChunks; i++ )
328✔
3169
  {
3170
    QString chunkID = CoreUtils::uuidWithoutBraces( QUuid::createUuid() );
165✔
3171
    chunks.append( chunkID );
165✔
3172
  }
165✔
3173
  return chunks;
163✔
3174
}
×
3175

3176
QJsonArray MerginApi::prepareUploadChangesJSON( const QList<MerginFile> &files )
345✔
3177
{
3178
  QJsonArray jsonArray;
345✔
3179

3180
  for ( MerginFile file : files )
500✔
3181
  {
3182
    QJsonObject fileObject;
155✔
3183
    fileObject.insert( "path", file.path );
155✔
3184

3185
    fileObject.insert( "size", file.size );
155✔
3186
    fileObject.insert( "mtime", file.mtime.toString( Qt::ISODateWithMs ) );
155✔
3187

3188
    if ( !file.diffName.isEmpty() )
155✔
3189
    {
3190
      // doing diff-based upload
3191
      QJsonObject diffObject;
12✔
3192
      diffObject.insert( "path", file.diffName );
12✔
3193
      diffObject.insert( "checksum", file.diffChecksum );
12✔
3194
      diffObject.insert( "size", file.diffSize );
12✔
3195

3196
      fileObject.insert( "diff", diffObject );
12✔
3197
      fileObject.insert( "checksum", file.diffBaseChecksum );
12✔
3198
    }
12✔
3199
    else
3200
    {
3201
      fileObject.insert( "checksum", file.checksum );
143✔
3202
    }
3203

3204
    QJsonArray chunksJson;
155✔
3205
    for ( QString id : file.chunks )
308✔
3206
    {
3207
      chunksJson.append( id );
153✔
3208
    }
153✔
3209
    fileObject.insert( "chunks", chunksJson );
155✔
3210
    jsonArray.append( fileObject );
155✔
3211
  }
155✔
3212
  return jsonArray;
345✔
3213
}
×
3214

3215
void MerginApi::finishProjectSync( const QString &projectFullName, bool syncSuccessful )
243✔
3216
{
3217
  Q_ASSERT( mTransactionalStatus.contains( projectFullName ) );
243✔
3218
  TransactionStatus &transaction = mTransactionalStatus[projectFullName];
243✔
3219

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

3222
  if ( syncSuccessful )
243✔
3223
  {
3224
    // update the local metadata file
3225
    writeData( transaction.projectMetadata, transaction.projectDir + "/" + MerginApi::sMetadataFile );
238✔
3226

3227
    // update info of local projects
3228
    mLocalProjects.updateLocalVersion( transaction.projectDir, transaction.version );
238✔
3229

3230
    CoreUtils::log( "sync " + projectFullName, QStringLiteral( "### Finished ###  New project version: %1\n" ).arg( transaction.version ) );
238✔
3231
  }
3232
  else
3233
  {
3234
    CoreUtils::log( "sync " + projectFullName, QStringLiteral( "### FAILED ###\n" ) );
5✔
3235
  }
3236

3237
  bool pullBeforePush = transaction.pullBeforePush;
243✔
3238
  QString projectDir = transaction.projectDir;  // keep it before the transaction gets removed
243✔
3239
  ProjectDiff diff = transaction.diff;
243✔
3240
  int newVersion = syncSuccessful ? transaction.version : -1;
243✔
3241

3242
  if ( transaction.gpkgSchemaChanged || projectFileHasBeenUpdated( diff ) )
243✔
3243
  {
3244
    emit projectReloadNeededAfterSync( projectFullName );
96✔
3245
  }
3246

3247
  mTransactionalStatus.remove( projectFullName );
243✔
3248

3249
  if ( pullBeforePush )
243✔
3250
  {
3251
    CoreUtils::log( "sync " + projectFullName, QStringLiteral( "Continue with push after pull" ) );
26✔
3252
    // we're done only with the download part before the actual upload - so let's continue with upload
3253
    QString projectNamespace, projectName;
26✔
3254
    extractProjectName( projectFullName, projectNamespace, projectName );
26✔
3255
    pushProject( projectNamespace, projectName );
26✔
3256
  }
26✔
3257
  else
3258
  {
3259
    emit syncProjectFinished( projectFullName, syncSuccessful, newVersion );
217✔
3260

3261
    if ( syncSuccessful )
217✔
3262
    {
3263
      emit projectDataChanged( projectFullName );
212✔
3264
    }
3265
  }
3266
}
243✔
3267

3268
bool MerginApi::writeData( const QByteArray &data, const QString &path )
238✔
3269
{
3270
  QFile file( path );
238✔
3271
  createPathIfNotExists( path );
238✔
3272
  if ( !file.open( QIODevice::WriteOnly ) )
238✔
3273
  {
3274
    return false;
×
3275
  }
3276

3277
  file.write( data );
238✔
3278
  file.close();
238✔
3279

3280
  return true;
238✔
3281
}
238✔
3282

3283

3284
void MerginApi::createPathIfNotExists( const QString &filePath )
706✔
3285
{
3286
  QDir dir;
706✔
3287
  if ( !dir.exists( mDataDir ) )
706✔
3288
    dir.mkpath( mDataDir );
×
3289

3290
  QFileInfo newFile( filePath );
706✔
3291
  if ( !newFile.absoluteDir().exists() )
706✔
3292
  {
3293
    if ( !dir.mkpath( newFile.absolutePath() ) )
160✔
3294
    {
3295
      CoreUtils::log( "create path", QString( "Creating a folder failed for path: %1" ).arg( filePath ) );
×
3296
    }
3297
  }
3298
}
706✔
3299

3300
bool MerginApi::isInIgnore( const QFileInfo &info )
1,659✔
3301
{
3302
  return sIgnoreExtensions.contains( info.suffix() ) || sIgnoreFiles.contains( info.fileName() ) || info.filePath().contains( sMetadataFolder + "/" );
1,659✔
3303
}
3304

3305
bool MerginApi::excludeFromSync( const QString &filePath, const MerginConfig &config )
373✔
3306
{
3307
  if ( config.isValid && config.selectiveSyncEnabled )
373✔
3308
  {
3309
    QFileInfo info( filePath );
243✔
3310

3311
    bool isExcludedFormat = sIgnoreImageExtensions.contains( info.suffix().toLower() );
243✔
3312

3313
    if ( !isExcludedFormat )
243✔
3314
      return false;
20✔
3315

3316
    if ( config.selectiveSyncDir.isEmpty() )
223✔
3317
    {
3318
      return true; // we are ignoring photos in the entire project
96✔
3319
    }
3320
    else if ( filePath.startsWith( config.selectiveSyncDir ) )
127✔
3321
    {
3322
      return true; // we are ignoring photo in subfolder
119✔
3323
    }
3324
  }
243✔
3325
  return false;
138✔
3326
}
3327

3328
QSet<QString> MerginApi::listFiles( const QString &path )
585✔
3329
{
3330
  QSet<QString> files;
585✔
3331
  QDirIterator it( path, QStringList() << QStringLiteral( "*" ), QDir::Files, QDirIterator::Subdirectories );
1,755✔
3332
  while ( it.hasNext() )
2,244✔
3333
  {
3334
    it.next();
1,659✔
3335
    if ( !isInIgnore( it.fileInfo() ) )
1,659✔
3336
    {
3337
      files << it.filePath().replace( path, "" );
1,659✔
3338
    }
3339
  }
3340
  return files;
1,170✔
3341
}
585✔
3342

3343
void MerginApi::deleteAccount()
1✔
3344
{
3345
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
1✔
3346
  {
3347
    return;
×
3348
  }
3349

3350
  QNetworkRequest request = getDefaultRequest();
1✔
3351
  QUrl url( mApiRoot + QStringLiteral( "/v1/user" ) );
2✔
3352
  request.setUrl( url );
1✔
3353
  QNetworkReply *reply = mManager.deleteResource( request );
1✔
3354
  connect( reply, &QNetworkReply::finished, this, [this]() { this->deleteAccountFinished();} );
2✔
3355
  CoreUtils::log( "delete account " + mUserAuth->username(), QStringLiteral( "Requesting account deletion: " ) + url.toString() );
2✔
3356
}
1✔
3357

3358
void MerginApi::deleteAccountFinished()
1✔
3359
{
3360
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
1✔
3361
  Q_ASSERT( r );
1✔
3362

3363
  if ( r->error() == QNetworkReply::NoError )
1✔
3364
  {
3365
    CoreUtils::log( "delete account " + mUserAuth->username(), QStringLiteral( "Success" ) );
1✔
3366

3367
    // remove all local projects from the device
3368
    LocalProjectsList projects = mLocalProjects.projects();
1✔
3369
    for ( const LocalProject &info : projects )
36✔
3370
    {
3371
      mLocalProjects.removeLocalProject( info.id() );
35✔
3372
    }
3373

3374
    clearAuth();
1✔
3375

3376
    emit accountDeleted( true );
1✔
3377
  }
1✔
3378
  else
3379
  {
3380
    int statusCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
×
3381
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
3382
    CoreUtils::log( "delete account " + mUserAuth->username(), QStringLiteral( "FAILED - %1 %2. %3" ).arg( statusCode ).arg( r->errorString() ).arg( serverMsg ) );
×
3383
    if ( statusCode == 422 )
×
3384
    {
3385
      emit userIsAnOrgOwnerError();
×
3386
    }
3387
    else
3388
    {
3389
      emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: deleteAccount" ) );
×
3390
    }
3391

3392
    emit accountDeleted( false );
×
3393
  }
×
3394

3395
  r->deleteLater();
1✔
3396
}
1✔
3397

3398
void MerginApi::getServerConfig()
25✔
3399
{
3400
  QNetworkRequest request = getDefaultRequest();
25✔
3401
  QString urlString = mApiRoot + QStringLiteral( "/config" );
50✔
3402
  QUrl url( urlString );
25✔
3403
  request.setUrl( url );
25✔
3404

3405
  QNetworkReply *reply = mManager.get( request );
25✔
3406

3407
  connect( reply, &QNetworkReply::finished, this, &MerginApi::getServerConfigReplyFinished );
25✔
3408
  CoreUtils::log( "Config", QStringLiteral( "Requesting server configuration: " ) + url.toString() );
50✔
3409
}
25✔
3410

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

3416
  if ( r->error() == QNetworkReply::NoError )
12✔
3417
  {
3418
    CoreUtils::log( "Config", QStringLiteral( "Success" ) );
12✔
3419
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
12✔
3420
    if ( doc.isObject() )
12✔
3421
    {
3422
      QString serverType = doc.object().value( QStringLiteral( "server_type" ) ).toString();
36✔
3423
      if ( serverType == QStringLiteral( "ee" ) )
12✔
3424
      {
3425
        setServerType( MerginServerType::EE );
×
3426
      }
3427
      else if ( serverType == QStringLiteral( "ce" ) )
12✔
3428
      {
3429
        setServerType( MerginServerType::CE );
×
3430
      }
3431
      else if ( serverType == QStringLiteral( "saas" ) )
12✔
3432
      {
3433
        setServerType( MerginServerType::SAAS );
12✔
3434
      }
3435
    }
12✔
3436
  }
12✔
3437
  else
3438
  {
3439
    int statusCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
×
3440
    if ( statusCode == 404 ) // legacy (old) server
×
3441
    {
3442
      setServerType( MerginServerType::OLD );
×
3443
    }
3444
    else
3445
    {
3446
      QString serverMsg = extractServerErrorMsg( r->readAll() );
×
3447
      QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "getServerType" ), r->errorString(), serverMsg );
×
3448
      emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: getServerType" ) );
×
3449
      CoreUtils::log( "server type", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
3450
    }
×
3451
  }
3452

3453
  r->deleteLater();
12✔
3454
}
12✔
3455

3456
MerginServerType::ServerType MerginApi::serverType() const
13✔
3457
{
3458
  return mServerType;
13✔
3459
}
3460

3461
void MerginApi::setServerType( const MerginServerType::ServerType &serverType )
17✔
3462
{
3463
  if ( mServerType != serverType )
17✔
3464
  {
3465
    if ( mServerType == MerginServerType::OLD && serverType == MerginServerType::SAAS )
6✔
3466
    {
3467
      emit serverWasUpgraded();
2✔
3468
    }
3469

3470
    mServerType = serverType;
6✔
3471
    QSettings settings;
6✔
3472
    settings.beginGroup( QStringLiteral( "Input/" ) );
6✔
3473
    settings.setValue( QStringLiteral( "serverType" ), mServerType );
12✔
3474
    settings.endGroup();
6✔
3475
    emit serverTypeChanged();
6✔
3476
    emit apiSupportsWorkspacesChanged();
6✔
3477
  }
6✔
3478
}
17✔
3479

3480
void MerginApi::listWorkspaces()
×
3481
{
3482
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
×
3483
  {
3484
    emit listWorkspacesFailed();
×
3485
    return;
×
3486
  }
3487

3488
  QUrl url( mApiRoot + QStringLiteral( "/v1/workspaces" ) );
×
3489
  QNetworkRequest request = getDefaultRequest( mUserAuth->hasAuthData() );
×
3490
  request.setUrl( url );
×
3491

3492
  QNetworkReply *reply = mManager.get( request );
×
3493
  CoreUtils::log( "list workspaces", QStringLiteral( "Requesting: " ) + url.toString() );
×
3494
  connect( reply, &QNetworkReply::finished, this, &MerginApi::listWorkspacesReplyFinished );
×
3495
}
×
3496

3497
void MerginApi::listWorkspacesReplyFinished()
×
3498
{
3499
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
×
3500
  Q_ASSERT( r );
×
3501

3502
  if ( r->error() == QNetworkReply::NoError )
×
3503
  {
3504
    CoreUtils::log( "list workspaces", QStringLiteral( "Success" ) );
×
3505
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
×
3506
    if ( doc.isArray() )
×
3507
    {
3508
      QMap<int, QString> workspaces;
×
3509
      QJsonArray array = doc.array();
×
3510
      for ( auto it = array.constBegin(); it != array.constEnd(); ++it )
×
3511
      {
3512
        QJsonObject ws = it->toObject();
×
3513
        workspaces.insert( ws.value( QStringLiteral( "id" ) ).toInt(), ws.value( QStringLiteral( "name" ) ).toString() );
×
3514
      }
×
3515

3516
      mUserInfo->setWorkspaces( workspaces );
×
3517
      emit listWorkspacesFinished( workspaces );
×
3518
    }
×
3519
    else
3520
    {
3521
      emit listWorkspacesFailed();
×
3522
    }
3523
  }
×
3524
  else
3525
  {
3526
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
3527
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listWorkspaces" ), r->errorString(), serverMsg );
×
3528
    CoreUtils::log( "list workspaces", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
3529
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: listWorkspaces" ) );
×
3530
    emit listWorkspacesFailed();
×
3531
  }
×
3532

3533
  r->deleteLater();
×
3534
}
×
3535

3536
void MerginApi::listInvitations()
×
3537
{
3538
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
×
3539
  {
3540
    emit listInvitationsFailed();
×
3541
    return;
×
3542
  }
3543

3544
  QUrl url( mApiRoot + QStringLiteral( "/v1/workspace/invitations" ) );
×
3545
  QNetworkRequest request = getDefaultRequest( mUserAuth->hasAuthData() );
×
3546
  request.setUrl( url );
×
3547

3548
  QNetworkReply *reply = mManager.get( request );
×
3549
  CoreUtils::log( "list invitations", QStringLiteral( "Requesting: " ) + url.toString() );
×
3550
  connect( reply, &QNetworkReply::finished, this, &MerginApi::listInvitationsReplyFinished );
×
3551
}
×
3552

3553
void MerginApi::listInvitationsReplyFinished()
×
3554
{
3555
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
×
3556
  Q_ASSERT( r );
×
3557

3558
  if ( r->error() == QNetworkReply::NoError )
×
3559
  {
3560
    CoreUtils::log( "list invitations", QStringLiteral( "Success" ) );
×
3561
    QJsonDocument doc = QJsonDocument::fromJson( r->readAll() );
×
3562
    if ( doc.isArray() )
×
3563
    {
3564
      QList<MerginInvitation> invitations;
×
3565
      QJsonArray array = doc.array();
×
3566
      for ( auto it = array.constBegin(); it != array.constEnd(); ++it )
×
3567
      {
3568
        MerginInvitation invite = MerginInvitation::fromJsonObject( it->toObject() );
×
3569
        invitations.append( invite );
×
3570
      }
×
3571

3572
      emit listInvitationsFinished( invitations );
×
3573
    }
×
3574
    else
3575
    {
3576
      emit listInvitationsFailed();
×
3577
    }
3578
  }
×
3579
  else
3580
  {
3581
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
3582
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listInvitations" ), r->errorString(), serverMsg );
×
3583
    CoreUtils::log( "list invitations", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
3584
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: listInvitations" ) );
×
3585
    emit listInvitationsFailed();
×
3586
  }
×
3587

3588
  r->deleteLater();
×
3589
}
×
3590

3591
void MerginApi::processInvitation( const QString &uuid, bool accept )
×
3592
{
3593
  if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK )
×
3594
  {
3595
    emit processInvitationFailed();
×
3596
    return;
×
3597
  }
3598

3599
  QNetworkRequest request = getDefaultRequest( true );
×
3600
  QString urlString = mApiRoot + QStringLiteral( "v1/workspace/invitation/%1" ).arg( uuid );
×
3601
  QUrl url( urlString );
×
3602
  request.setUrl( url );
×
3603
  request.setRawHeader( "Content-Type", "application/json" );
×
3604
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrAcceptFlag ), accept );
×
3605

3606
  QJsonDocument jsonDoc;
×
3607
  QJsonObject jsonObject;
×
3608
  jsonObject.insert( QStringLiteral( "accept" ), accept );
×
3609
  jsonDoc.setObject( jsonObject );
×
3610
  QByteArray json = jsonDoc.toJson( QJsonDocument::Compact );
×
3611
  QNetworkReply *reply = mManager.post( request, json );
×
3612
  CoreUtils::log( "process invitation", QStringLiteral( "Requesting: " ) + url.toString() );
×
3613
  connect( reply, &QNetworkReply::finished, this, &MerginApi::processInvitationReplyFinished );
×
3614
}
×
3615

3616
void MerginApi::processInvitationReplyFinished()
×
3617
{
3618
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
×
3619
  Q_ASSERT( r );
×
3620

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

3623
  if ( r->error() == QNetworkReply::NoError )
×
3624
  {
3625
    CoreUtils::log( "process invitation", QStringLiteral( "Success" ) );
×
3626
  }
3627
  else
3628
  {
3629
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
3630
    QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "processInvitation" ), r->errorString(), serverMsg );
×
3631
    CoreUtils::log( "process invitation", QStringLiteral( "FAILED - %1" ).arg( message ) );
×
3632
    emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: processInvitation" ) );
×
3633
    emit processInvitationFailed();
×
3634
  }
×
3635

3636
  emit processInvitationFinished( accept );
×
3637

3638
  r->deleteLater();
×
3639
}
×
3640

3641
bool MerginApi::createWorkspace( const QString &workspaceName )
2✔
3642
{
3643
  if ( !validateAuth() )
2✔
3644
  {
3645
    emit missingAuthorizationError( workspaceName );
×
3646
    return false;
×
3647
  }
3648

3649
  if ( mApiVersionStatus != MerginApiStatus::OK )
2✔
3650
  {
3651
    return false;
×
3652
  }
3653

3654
  if ( !CoreUtils::isValidName( workspaceName ) )
2✔
3655
  {
3656
    emit notify( tr( "Workspace name contains invalid characters" ) );
×
3657
    return false;
×
3658
  }
3659

3660
  QNetworkRequest request = getDefaultRequest();
2✔
3661
  QUrl url( mApiRoot + QString( "/v1/workspace" ) );
4✔
3662
  request.setUrl( url );
2✔
3663
  request.setRawHeader( "Content-Type", "application/json" );
2✔
3664
  request.setRawHeader( "Accept", "application/json" );
2✔
3665
  request.setAttribute( static_cast<QNetworkRequest::Attribute>( AttrWorkspaceName ), workspaceName );
2✔
3666

3667
  QJsonDocument jsonDoc;
2✔
3668
  QJsonObject jsonObject;
2✔
3669
  jsonObject.insert( QStringLiteral( "name" ), workspaceName );
4✔
3670
  jsonDoc.setObject( jsonObject );
2✔
3671
  QByteArray json = jsonDoc.toJson( QJsonDocument::Compact );
2✔
3672

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

3677
  return true;
2✔
3678
}
2✔
3679

3680
void MerginApi::signOut()
×
3681
{
3682
  clearAuth();
×
3683
}
×
3684

3685
void MerginApi::refreshUserData()
×
3686
{
3687
  getUserInfo();
×
3688

3689
  if ( apiSupportsWorkspaces() )
×
3690
  {
3691
    getWorkspaceInfo();
×
3692
    // getServiceInfo is called automatically when workspace info finishes
3693
  }
3694
  else if ( mServerType == MerginServerType::OLD )
×
3695
  {
3696
    getServiceInfo();
×
3697
  }
3698
}
×
3699

3700
void MerginApi::createWorkspaceReplyFinished()
2✔
3701
{
3702
  QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
2✔
3703
  Q_ASSERT( r );
2✔
3704

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

3707
  if ( r->error() == QNetworkReply::NoError )
2✔
3708
  {
3709
    CoreUtils::log( "create " + workspaceName, QStringLiteral( "Success" ) );
2✔
3710
    emit workspaceCreated( workspaceName, true );
2✔
3711
  }
3712
  else
3713
  {
3714
    QString serverMsg = extractServerErrorMsg( r->readAll() );
×
3715
    QString message = QStringLiteral( "FAILED - %1: %2" ).arg( r->errorString(), serverMsg );
×
3716
    CoreUtils::log( "create " + workspaceName, message );
×
3717

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

3720
    if ( httpCode == 409 )
×
3721
    {
3722
      emit networkErrorOccurred( tr( "Workspace %1 already exists" ).arg( workspaceName ), QStringLiteral( "Mergin API error: createWorkspace" ), httpCode, workspaceName );
×
3723
    }
3724
    else
3725
    {
3726
      emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: createWorkspace" ), httpCode, workspaceName );
×
3727
    }
3728
    emit workspaceCreated( workspaceName, false );
×
3729
  }
×
3730
  r->deleteLater();
2✔
3731
}
2✔
3732

3733
bool MerginApi::apiSupportsWorkspaces()
1✔
3734
{
3735
  if ( mServerType == MerginServerType::SAAS || mServerType == MerginServerType::EE )
1✔
3736
  {
3737
    return true;
1✔
3738
  }
3739
  else
3740
  {
3741
    return false;
×
3742
  }
3743
}
3744

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

© 2026 Coveralls, Inc