diff --git a/app/activeproject.cpp b/app/activeproject.cpp index 20e326054..823043eed 100644 --- a/app/activeproject.cpp +++ b/app/activeproject.cpp @@ -9,7 +9,6 @@ #include #include -#include #include "qgsvectorlayer.h" #include "qgslayertree.h" @@ -91,12 +90,17 @@ QString ActiveProject::projectFullName() const return mLocalProject.fullName(); } +QString ActiveProject::projectId() const +{ + return mLocalProject.id(); +} + bool ActiveProject::load( const QString &filePath ) { return forceLoad( filePath, false ); } -bool ActiveProject::forceLoad( const QString &filePath, bool force ) +bool ActiveProject::forceLoad( const QString &filePath, const bool force ) { CoreUtils::log( QStringLiteral( "Project loading" ), filePath + " " + ( force ? "true" : "false" ) ); @@ -133,11 +137,11 @@ bool ActiveProject::forceLoad( const QString &filePath, bool force ) mProjectLoadingLog.clear(); - QString logFilePath = CoreUtils::logFilename(); + const QString logFilePath = CoreUtils::logFilename(); qint64 alreadyAppendedCharsCount = 0; { - QFile file( logFilePath ); + const QFile file( logFilePath ); alreadyAppendedCharsCount = file.size(); } @@ -157,13 +161,13 @@ bool ActiveProject::forceLoad( const QString &filePath, bool force ) res = mQgsProject->read( filePath ); if ( !res ) { - QString error = mQgsProject->error(); + const QString error = mQgsProject->error(); CoreUtils::log( QStringLiteral( "Project loading" ), QStringLiteral( "Could not read project file: " ) + error ); mLocalProject = LocalProject(); if ( mMapSettings ) { - QList< QgsMapLayer * > layers; + const QList< QgsMapLayer * > layers; mMapSettings->setLayers( layers ); } mQgsProject->clear(); @@ -182,7 +186,7 @@ bool ActiveProject::forceLoad( const QString &filePath, bool force ) CoreUtils::log( QStringLiteral( "Project load" ), QStringLiteral( "Could not find project in local projects: " ) + filePath ); } - QString role = MerginProjectMetadata::fromCachedJson( CoreUtils::getProjectMetadataPath( mLocalProject.projectDir ) ).role; + const QString role = MerginProjectMetadata::fromCachedJson( CoreUtils::getProjectMetadataPath( mLocalProject.projectDir ) ).role; setProjectRole( role ); updateMapTheme(); @@ -195,7 +199,7 @@ bool ActiveProject::forceLoad( const QString &filePath, bool force ) emit mapSketchesEnabledChanged(); } - bool foundErrorsInLoadedProject = validateProject(); + const bool foundErrorsInLoadedProject = validateProject(); flagFile.remove(); if ( !force ) @@ -208,7 +212,7 @@ bool ActiveProject::forceLoad( const QString &filePath, bool force ) if ( file.open( QIODevice::ReadOnly ) ) { file.seek( alreadyAppendedCharsCount ); - QByteArray neededLogFileData = file.readAll(); + const QByteArray neededLogFileData = file.readAll(); mProjectLoadingLog = QString::fromStdString( neededLogFileData.toStdString() ); file.close(); } @@ -266,7 +270,7 @@ bool ActiveProject::validateProject() // B. Per-Layer validations QMap projectLayers = mQgsProject->mapLayers(); - for ( QgsMapLayer *layer : projectLayers ) + for ( const QgsMapLayer *layer : projectLayers ) { // B.1. Layer Validity if ( !layer->isValid() ) @@ -290,7 +294,7 @@ bool ActiveProject::validateProject() return errorsFound; } -bool ActiveProject::reloadProject( QString projectDir ) +bool ActiveProject::reloadProject( const QString &projectDir ) { if ( mQgsProject->homePath() == projectDir ) { @@ -299,7 +303,7 @@ bool ActiveProject::reloadProject( QString projectDir ) if ( mMapSettings ) extent = mMapSettings->extent(); - bool result = forceLoad( mQgsProject->fileName(), true ); + const bool result = forceLoad( mQgsProject->fileName(), true ); // restore extent if ( mMapSettings && !extent.isNull() ) @@ -310,7 +314,7 @@ bool ActiveProject::reloadProject( QString projectDir ) return false; } -void ActiveProject::setAutosyncEnabled( bool enabled ) +void ActiveProject::setAutosyncEnabled( const bool enabled ) { if ( enabled ) { @@ -354,7 +358,7 @@ void ActiveProject::updateMapSettingsLayers() const { if ( !mQgsProject || !mMapSettings ) return; - QList visibleLayers = getVisibleLayers(); + const QList visibleLayers = getVisibleLayers(); mMapSettings->setLayers( visibleLayers ); mMapSettings->setTransformContext( mQgsProject->transformContext() ); } @@ -387,7 +391,7 @@ void ActiveProject::updateMapTheme() } QgsLayerTree *root = mQgsProject->layerTreeRoot(); - QgsMapThemeCollection *collection = mQgsProject->mapThemeCollection(); + const QgsMapThemeCollection *collection = mQgsProject->mapThemeCollection(); if ( !root || !collection ) { @@ -399,7 +403,7 @@ void ActiveProject::updateMapTheme() QString themeCandidateName; QStringList mapThemes = collection->mapThemes(); - QgsMapThemeCollection::MapThemeRecord themeCandidate = collection->createThemeFromCurrentState( root, &model ); + const QgsMapThemeCollection::MapThemeRecord themeCandidate = collection->createThemeFromCurrentState( root, &model ); for ( const QString &themeName : mapThemes ) { @@ -449,7 +453,7 @@ void ActiveProject::setMapTheme( const QString &themeName ) updateMapSettingsLayers(); } -void ActiveProject::updateActiveLayer() +void ActiveProject::updateActiveLayer() const { QList< QgsMapLayer * > visibleLayers = getVisibleLayers(); @@ -554,7 +558,7 @@ void ActiveProject::setProjectRole( const QString &role ) } } -bool ActiveProject::recordingAllowed( QgsMapLayer *layer ) const +bool ActiveProject::recordingAllowed( const QgsMapLayer *layer ) const { if ( !layer ) return false; @@ -580,7 +584,7 @@ QList ActiveProject::getVisibleLayers() const if ( !mQgsProject ) return QList(); - QgsLayerTree *root = mQgsProject->layerTreeRoot(); + const QgsLayerTree *root = mQgsProject->layerTreeRoot(); if ( !root ) return QList(); @@ -589,7 +593,7 @@ QList ActiveProject::getVisibleLayers() const QList visibleLayers; const QList nodeLayers = root->findLayers(); - for ( QgsLayerTreeLayer *nodeLayer : nodeLayers ) + for ( const QgsLayerTreeLayer *nodeLayer : nodeLayers ) { if ( nodeLayer && nodeLayer->isVisible() ) { diff --git a/app/activeproject.h b/app/activeproject.h index c685d4dbf..40c030fb6 100644 --- a/app/activeproject.h +++ b/app/activeproject.h @@ -15,14 +15,12 @@ #include "qgsproject.h" -#include "inputconfig.h" #include "appsettings.h" #include "activelayer.h" #include "recordinglayersproxymodel.h" #include "localprojectsmanager.h" #include "autosynccontroller.h" #include "inputmapsettings.h" -#include "merginprojectmetadata.h" /** * \brief The ActiveProject class can load a QGIS project and holds its data. @@ -47,7 +45,7 @@ class ActiveProject: public QObject , LocalProjectsManager &localProjectsManager , QObject *parent = nullptr ); - virtual ~ActiveProject(); + ~ActiveProject() override; //! Returns active project's QgsProject instance to do QGIS API magic QgsProject *qgsProject() const; @@ -57,6 +55,8 @@ class ActiveProject: public QObject Q_INVOKABLE QString projectFullName() const; + Q_INVOKABLE QString projectId() const; + /** * Loads a .qgz/.qgs project file specified by filePath. * \param filePath Path to project file. @@ -64,10 +64,10 @@ class ActiveProject: public QObject Q_INVOKABLE bool load( const QString &filePath ); /** - * Applies map theme with 'name' to currently loaded QGIS project + * Applies map theme with 'themeName' to currently loaded QGIS project * Invalidates active layer if it is no longer visible */ - Q_INVOKABLE void setMapTheme( const QString &name ); + Q_INVOKABLE void setMapTheme( const QString &themeName ); /** * setActiveLayer sets active layer from layer @@ -128,7 +128,7 @@ class ActiveProject: public QObject * Returns if project layer allows recording (has geometry, editable, not position tracking layer, not map * sketching layer) regardless of visibility */ - bool recordingAllowed( QgsMapLayer *layer ) const ; + bool recordingAllowed( const QgsMapLayer *layer ) const ; //! Returns position tracking layer ID if exists Q_INVOKABLE QString positionTrackingLayerId() const; @@ -165,7 +165,7 @@ class ActiveProject: public QObject void positionTrackingSupportedChanged(); - // Emited when the app (UI) should show tracking because there is a running tracking service + // Emitted when the app (UI) should show tracking because there is a running tracking service void startPositionTracking(); void projectRoleChanged(); @@ -174,7 +174,7 @@ class ActiveProject: public QObject public slots: // Reloads project if current project path matches given path (its the same project) - bool reloadProject( QString projectDir ); + bool reloadProject( const QString &projectDir ); void setAutosyncEnabled( bool enabled ); @@ -196,10 +196,7 @@ class ActiveProject: public QObject * if not, sets first available layer as active; * sets nullptr if there are no other available layers */ - void updateActiveLayer(); - - //! Reloads layers in 'recoring layers model' - void updateRecordingLayers(); + void updateActiveLayer() const; QgsProject *mQgsProject = nullptr; LocalProject mLocalProject; diff --git a/app/main.cpp b/app/main.cpp index 5537076a0..81079db7a 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -586,20 +586,20 @@ int main( int argc, char *argv[] ) QObject::connect( &activeProject, &ActiveProject::projectReloaded, &lambdaContext, [merginApi = ma.get(), &activeProject]() { - merginApi->reloadProjectRole( activeProject.projectFullName() ); + merginApi->reloadProjectRole( activeProject.projectId() ); } ); QObject::connect( ma.get(), &MerginApi::authChanged, &lambdaContext, [merginApi = ma.get(), &activeProject]() { if ( activeProject.isProjectLoaded() ) { - merginApi->reloadProjectRole( activeProject.projectFullName() ); + merginApi->reloadProjectRole( activeProject.projectId() ); } } ); - QObject::connect( ma.get(), &MerginApi::projectRoleUpdated, &activeProject, [&activeProject]( const QString & projectFullName, const QString & role ) + QObject::connect( ma.get(), &MerginApi::projectRoleUpdated, &activeProject, [&activeProject]( const QString & projectId, const QString & role ) { - if ( projectFullName == activeProject.projectFullName() ) + if ( projectId == activeProject.projectId() ) { activeProject.setProjectRole( role ); } @@ -624,11 +624,11 @@ int main( int argc, char *argv[] ) QObject::connect( &pw, &ProjectWizard::projectCreated, &localProjectsManager, &LocalProjectsManager::addLocalProject ); QObject::connect( &activeProject, &ActiveProject::projectReloaded, vm.get(), &VariablesManager::merginProjectChanged ); QObject::connect( &activeProject, &ActiveProject::projectWillBeReloaded, &inputProjUtils, &InputProjUtils::resetHandlers ); - QObject::connect( &syncManager, &SynchronizationManager::syncFinished, &activeProject, [&activeProject]( const QString & projectFullName, bool successfully, int version, bool reloadNeeded ) + QObject::connect( &syncManager, &SynchronizationManager::syncFinished, &activeProject, [&activeProject]( const QString & projectId, const bool successfully, const int version, const bool reloadNeeded ) { Q_UNUSED( successfully ); Q_UNUSED( version ); - if ( reloadNeeded && activeProject.projectFullName() == projectFullName ) + if ( reloadNeeded && activeProject.projectId() == projectId ) { activeProject.reloadProject( activeProject.qgsProject()->homePath() ); } @@ -680,7 +680,7 @@ int main( int argc, char *argv[] ) // and properly close connection after writting changes to gpkg. qputenv( "OGR_SQLITE_JOURNAL", "DELETE" ); - + // TODO: rework to singletons instead (check Qt docs why) // Register to QQmlEngine engine.rootContext()->setContextProperty( "__notificationModel", ¬ificationModel ); engine.rootContext()->setContextProperty( "__androidUtils", &androidUtils ); diff --git a/app/projectsmodel.cpp b/app/projectsmodel.cpp index 26a58519a..64361309a 100644 --- a/app/projectsmodel.cpp +++ b/app/projectsmodel.cpp @@ -27,44 +27,45 @@ void ProjectsModel::initializeProjectsModel() if ( !mSyncManager || !mBackend || !mLocalProjectsManager || mModelType == EmptyProjectsModel ) // Model is not set up properly yet return; - QObject::connect( mSyncManager, &SynchronizationManager::syncStarted, this, &ProjectsModel::onProjectSyncStarted ); - QObject::connect( mSyncManager, &SynchronizationManager::syncFinished, this, &ProjectsModel::onProjectSyncFinished ); - QObject::connect( mSyncManager, &SynchronizationManager::syncCancelled, this, &ProjectsModel::onProjectSyncCancelled ); - QObject::connect( mSyncManager, &SynchronizationManager::syncProgressChanged, this, &ProjectsModel::onProjectSyncProgressChanged ); + connect( mSyncManager, &SynchronizationManager::syncStarted, this, &ProjectsModel::onProjectSyncStarted ); + connect( mSyncManager, &SynchronizationManager::syncFinished, this, &ProjectsModel::onProjectSyncFinished ); + connect( mSyncManager, &SynchronizationManager::syncCancelled, this, &ProjectsModel::onProjectSyncCancelled ); + connect( mSyncManager, &SynchronizationManager::syncProgressChanged, this, &ProjectsModel::onProjectSyncProgressChanged ); - QObject::connect( mBackend, &MerginApi::projectDetached, this, &ProjectsModel::onProjectDetachedFromMergin ); - QObject::connect( mBackend, &MerginApi::projectAttachedToMergin, this, &ProjectsModel::onProjectAttachedToMergin ); - QObject::connect( mBackend, &MerginApi::authChanged, this, &ProjectsModel::onAuthChanged ); + connect( mBackend, &MerginApi::projectDetached, this, &ProjectsModel::onProjectDetachedFromMergin ); + connect( mBackend, &MerginApi::projectAttachedToMergin, this, &ProjectsModel::onProjectAttachedToMergin ); + connect( mBackend, &MerginApi::authChanged, this, &ProjectsModel::onAuthChanged ); if ( mModelType == ProjectModelTypes::LocalProjectsModel ) { - QObject::connect( mBackend, &MerginApi::listProjectsByNameFinished, this, &ProjectsModel::onListProjectsByNameFinished ); + connect( mBackend, &MerginApi::listProjectsByNameFinished, this, &ProjectsModel::onListProjectsByNameFinished ); + connect( mBackend, &MerginApi::refetchBrokenProjectsFinished, this, &ProjectsModel::onRefetchBrokenProjectsFinished ); loadLocalProjects(); } else if ( mModelType != ProjectModelTypes::RecentProjectsModel ) { - QObject::connect( mBackend, &MerginApi::listProjectsFinished, this, &ProjectsModel::onListProjectsFinished ); + connect( mBackend, &MerginApi::listProjectsFinished, this, &ProjectsModel::onListProjectsFinished ); } else { // Implement RecentProjectsModel type } - QObject::connect( mLocalProjectsManager, &LocalProjectsManager::localProjectAdded, this, &ProjectsModel::onProjectAdded ); - QObject::connect( mLocalProjectsManager, &LocalProjectsManager::aboutToRemoveLocalProject, this, &ProjectsModel::onAboutToRemoveProject ); - QObject::connect( mLocalProjectsManager, &LocalProjectsManager::localProjectDataChanged, this, &ProjectsModel::onProjectDataChanged ); - QObject::connect( mLocalProjectsManager, &LocalProjectsManager::dataDirReloaded, this, &ProjectsModel::loadLocalProjects ); + connect( mLocalProjectsManager, &LocalProjectsManager::localProjectAdded, this, &ProjectsModel::onProjectAdded ); + connect( mLocalProjectsManager, &LocalProjectsManager::aboutToRemoveLocalProject, this, &ProjectsModel::onAboutToRemoveProject ); + connect( mLocalProjectsManager, &LocalProjectsManager::localProjectDataChanged, this, &ProjectsModel::onProjectDataChanged ); + connect( mLocalProjectsManager, &LocalProjectsManager::dataDirReloaded, this, &ProjectsModel::loadLocalProjects ); emit modelInitialized(); } -QVariant ProjectsModel::data( const QModelIndex &index, int role ) const +QVariant ProjectsModel::data( const QModelIndex &index, const int role ) const { if ( !index.isValid() ) - return QVariant(); + return {}; if ( index.row() < 0 || index.row() >= mProjects.size() ) - return QVariant(); + return {}; const Project project = mProjects.at( index.row() ); @@ -95,7 +96,7 @@ QVariant ProjectsModel::data( const QModelIndex &index, int role ) const } else { - ProjectStatus::Status status = ProjectStatus::projectStatus( project, mBackend->supportsSelectiveSync() ); + const ProjectStatus::Status status = ProjectStatus::projectStatus( project, mBackend->supportsSelectiveSync() ); if ( status == ProjectStatus::NeedsSync ) { @@ -108,7 +109,7 @@ QVariant ProjectsModel::data( const QModelIndex &index, int role ) const } } - QFileInfo fi( project.local.projectDir ); + const QFileInfo fi( project.local.projectDir ); // Up to date // lastModified of projectDir is not reliable - gpkg file may have modified header after opening it. See more #1320 @@ -121,7 +122,7 @@ QVariant ProjectsModel::data( const QModelIndex &index, int role ) const // This should not happen CoreUtils::log( "Project error", "Found project that is not downloaded nor remote" ); - return QVariant(); + return {}; } case ProjectIsActiveProject: { @@ -129,20 +130,20 @@ QVariant ProjectsModel::data( const QModelIndex &index, int role ) const } default: { - if ( !project.isMergin() ) return QVariant(); + if ( !project.isMergin() ) return {}; // Roles only for projects that has mergin part if ( role == ProjectSyncPending ) return QVariant( mSyncManager->hasPendingSync( project.fullName() ) ); else if ( role == ProjectSyncProgress ) return QVariant( mSyncManager->syncProgress( project.fullName() ) ); else if ( role == ProjectRemoteError ) return QVariant( project.mergin.remoteError ); - return QVariant(); + return {}; } } } -QModelIndex ProjectsModel::index( int row, int col, const QModelIndex &parent ) const +QModelIndex ProjectsModel::index( const int row, const int column, const QModelIndex &parent ) const { - Q_UNUSED( col ) + Q_UNUSED( column ) Q_UNUSED( parent ) return createIndex( row, 0, nullptr ); } @@ -170,14 +171,14 @@ QHash ProjectsModel::roleNames() const int ProjectsModel::rowCount( const QModelIndex & ) const { - return mProjects.count(); + return static_cast( mProjects.count() ); } -void ProjectsModel::listProjects( const QString &searchExpression, int page ) +void ProjectsModel::listProjects( const QString &searchExpression, const int page ) { if ( mModelType == LocalProjectsModel ) { - listProjectsByName(); + fetchProjectsByProjectId(); return; } @@ -192,14 +193,29 @@ void ProjectsModel::listProjects( const QString &searchExpression, int page ) } } -void ProjectsModel::listProjectsByName() +void ProjectsModel::fetchProjectsByProjectId( const QStringList &projectIds ) { if ( mModelType != LocalProjectsModel ) { return; } - mLastRequestId = mBackend->listProjectsByName( projectNames() ); + QStringList projectNamesList{}; + if ( projectIds.isEmpty() ) + { + for ( auto project : mLocalProjectsManager->projects().values() ) + { + projectNamesList << project.fullName(); + } + } + else + { + for ( auto key : projectIds ) + { + projectNamesList << mLocalProjectsManager->projectFromProjectId( key ).fullName(); + } + } + mLastRequestId = mBackend->listProjectsByName( projectNamesList ); if ( !mLastRequestId.isEmpty() ) { @@ -218,7 +234,7 @@ void ProjectsModel::fetchAnotherPage( const QString &searchExpression ) listProjects( searchExpression, mPaginatedPage + 1 ); } -void ProjectsModel::onListProjectsFinished( const MerginProjectsList &merginProjects, int projectsCount, int page, QString requestId ) +void ProjectsModel::onListProjectsFinished( const MerginProjectsList &merginProjects, const int projectsCount, const int page, const QString &requestId ) { if ( mLastRequestId != requestId ) { @@ -235,7 +251,7 @@ void ProjectsModel::onListProjectsFinished( const MerginProjectsList &merginProj else { // paginating next page, keep previous projects and emit model add items - beginInsertRows( QModelIndex(), mProjects.size(), mProjects.size() + merginProjects.size() - 1 ); + beginInsertRows( QModelIndex(), static_cast( mProjects.size() ), static_cast( mProjects.size() ) + merginProjects.size() - 1 ); mergeProjects( merginProjects, MergeStrategy::KeepPrevious ); endInsertRows(); } @@ -246,13 +262,20 @@ void ProjectsModel::onListProjectsFinished( const MerginProjectsList &merginProj setModelIsLoading( false ); } -void ProjectsModel::onListProjectsByNameFinished( const MerginProjectsList &merginProjects, QString requestId ) +void ProjectsModel::onListProjectsByNameFinished( const MerginProjectsList &merginProjects, const QString &requestId ) { if ( mLastRequestId != requestId ) { return; } + // filter out projects which returned errors and refetch them by ID + const QStringList brokenProjects = filterBrokenProjects( merginProjects ); + if ( !brokenProjects.isEmpty() ) + { + mBackend->refetchBrokenProjects( brokenProjects ); + } + beginResetModel(); mergeProjects( merginProjects ); endResetModel(); @@ -260,9 +283,21 @@ void ProjectsModel::onListProjectsByNameFinished( const MerginProjectsList &merg setModelIsLoading( false ); } +void ProjectsModel::onRefetchBrokenProjectsFinished( const MerginProjectsList &merginProjects ) +{ + if ( merginProjects.isEmpty() ) + { + return; + } + + beginResetModel(); + mergeProjects( merginProjects, MergeStrategy::KeepPrevious ); + endResetModel(); +} + void ProjectsModel::mergeProjects( const MerginProjectsList &merginProjects, MergeStrategy mergeStrategy ) { - const LocalProjectsList localProjects = mLocalProjectsManager->projects(); + const LocalProjectsDict localProjects = mLocalProjectsManager->projects(); if ( mergeStrategy == DiscardPrevious ) { @@ -272,7 +307,7 @@ void ProjectsModel::mergeProjects( const MerginProjectsList &merginProjects, Mer if ( mModelType == ProjectModelTypes::LocalProjectsModel ) { // Keep all local projects and ignore all not downloaded remote projects - for ( const LocalProject &localProject : localProjects ) + for ( const LocalProject &localProject : localProjects.values() ) { Project project; project.local = localProject; @@ -301,7 +336,7 @@ void ProjectsModel::mergeProjects( const MerginProjectsList &merginProjects, Mer mProjects << project; } - // lets check also for projects that are currently being downloaded and add them to local projects list + // let's check also for projects that are currently being downloaded and add them to local projects list QList pendingProjects = mSyncManager->pendingProjects(); for ( const QString &pendingProjectName : pendingProjects ) @@ -316,7 +351,7 @@ void ProjectsModel::mergeProjects( const MerginProjectsList &merginProjects, Mer { Project project; - MerginApi::extractProjectName( pendingProjectName, project.mergin.projectNamespace, project.mergin.projectName ); + CoreUtils::extractProjectName( pendingProjectName, project.mergin.projectNamespace, project.mergin.projectName ); project.mergin.status = ProjectStatus::projectStatus( project, mBackend->supportsSelectiveSync() ); mProjects << project; @@ -349,7 +384,7 @@ void ProjectsModel::mergeProjects( const MerginProjectsList &merginProjects, Mer void ProjectsModel::syncProject( const QString &projectId ) { - int ix = projectIndexFromId( projectId ); + const int ix = projectIndexFromId( projectId ); if ( ix < 0 ) return; @@ -367,7 +402,7 @@ void ProjectsModel::syncProject( const QString &projectId ) } } -void ProjectsModel::stopProjectSync( const QString &projectId ) +void ProjectsModel::stopProjectSync( const QString &projectId ) const { if ( mSyncManager ) { @@ -375,46 +410,46 @@ void ProjectsModel::stopProjectSync( const QString &projectId ) } } -void ProjectsModel::removeLocalProject( const QString &projectId ) +void ProjectsModel::removeLocalProject( const QString &projectId ) const { mLocalProjectsManager->removeLocalProject( projectId ); } void ProjectsModel::migrateProject( const QString &projectId ) { - int ix = projectIndexFromId( projectId ); + const int ix = projectIndexFromId( projectId ); if ( ix < 0 ) return; - mSyncManager->migrateProjectToMergin( mProjects[ix].local.projectName ); + mSyncManager->migrateProjectToMergin( mProjects[ix].local.projectName, projectId ); } -void ProjectsModel::onProjectSyncStarted( const QString &projectFullName ) +void ProjectsModel::onProjectSyncStarted( const QString &projectId ) { - int ix = projectIndexFromId( projectFullName ); + const int ix = projectIndexFromId( projectId ); if ( ix < 0 ) return; - QModelIndex changeIndex = index( ix ); + const QModelIndex changeIndex = index( ix ); emit dataChanged( changeIndex, changeIndex, { ProjectSyncPending, ProjectSyncProgress } ); } -void ProjectsModel::onProjectSyncCancelled( const QString &projectFullName ) +void ProjectsModel::onProjectSyncCancelled( const QString &projectId ) { - int ix = projectIndexFromId( projectFullName ); + const int ix = projectIndexFromId( projectId ); if ( ix < 0 ) return; - QModelIndex changeIndex = index( ix ); + const QModelIndex changeIndex = index( ix ); emit dataChanged( changeIndex, changeIndex, { ProjectSyncPending, ProjectSyncProgress, ProjectStatus } ); } -void ProjectsModel::onProjectSyncFinished( const QString &projectFullName, bool successfully, int newVersion ) +void ProjectsModel::onProjectSyncFinished( const QString &projectId, const bool successfully, const int newVersion ) { - int ix = projectIndexFromId( projectFullName ); + const int ix = projectIndexFromId( projectId ); if ( ix < 0 ) return; @@ -431,7 +466,7 @@ void ProjectsModel::onProjectSyncFinished( const QString &projectFullName, bool project.mergin.status = ProjectStatus::projectStatus( project, mBackend->supportsSelectiveSync() ); - QModelIndex changeIndex = index( ix ); + const QModelIndex changeIndex = index( ix ); emit dataChanged( changeIndex, changeIndex, { ProjectSyncPending, ProjectSyncProgress, ProjectStatus } ); // remove project from list of projects if this was a first-time download of remote project in local projects list @@ -446,11 +481,11 @@ void ProjectsModel::onProjectSyncFinished( const QString &projectFullName, bool } } -void ProjectsModel::onProjectSyncProgressChanged( const QString &projectFullName, qreal progress ) +void ProjectsModel::onProjectSyncProgressChanged( const QString &projectId, const qreal progress ) { Q_UNUSED( progress ) - int ix = projectIndexFromId( projectFullName ); + const int ix = projectIndexFromId( projectId ); if ( ix < 0 ) return; @@ -458,14 +493,14 @@ void ProjectsModel::onProjectSyncProgressChanged( const QString &projectFullName if ( !mProjects[ix].isMergin() ) return; - QModelIndex changeIndex = index( ix ); + const QModelIndex changeIndex = index( ix ); emit dataChanged( changeIndex, changeIndex, { ProjectSyncPending, ProjectSyncProgress } ); } void ProjectsModel::onProjectAdded( const LocalProject &localProject ) { // Check if such project is already in project list - int ix = projectIndexFromId( localProject.id() ); + const int ix = projectIndexFromId( localProject.id() ); if ( ix >= 0 ) { // add local information ~ project downloaded @@ -477,7 +512,7 @@ void ProjectsModel::onProjectAdded( const LocalProject &localProject ) project.mergin.status = ProjectStatus::projectStatus( project, mBackend->supportsSelectiveSync() ); } - QModelIndex modelIx = index( ix ); + const QModelIndex modelIx = index( ix ); emit dataChanged( modelIx, modelIx ); } else if ( mModelType == LocalProjectsModel ) @@ -486,7 +521,7 @@ void ProjectsModel::onProjectAdded( const LocalProject &localProject ) Project project; project.local = localProject; - int insertIndex = mProjects.size(); + const int insertIndex = static_cast( mProjects.size() ); beginInsertRows( QModelIndex(), insertIndex, insertIndex ); mProjects << project; @@ -496,7 +531,7 @@ void ProjectsModel::onProjectAdded( const LocalProject &localProject ) void ProjectsModel::onAboutToRemoveProject( const LocalProject &localProject ) { - int ix = projectIndexFromId( localProject.id() ); + const int ix = projectIndexFromId( localProject.id() ); if ( ix >= 0 ) { @@ -512,7 +547,7 @@ void ProjectsModel::onAboutToRemoveProject( const LocalProject &localProject ) mProjects[ix].local = LocalProject(); mProjects[ix].mergin.status = ProjectStatus::projectStatus( mProjects[ix], mBackend->supportsSelectiveSync() ); - QModelIndex modelIx = index( ix ); + const QModelIndex modelIx = index( ix ); emit dataChanged( modelIx, modelIx ); } } @@ -520,7 +555,7 @@ void ProjectsModel::onAboutToRemoveProject( const LocalProject &localProject ) void ProjectsModel::onProjectDataChanged( const LocalProject &localProject ) { - int ix = projectIndexFromId( localProject.id() ); + const int ix = projectIndexFromId( localProject.id() ); if ( ix < 0 ) return; @@ -534,13 +569,13 @@ void ProjectsModel::onProjectDataChanged( const LocalProject &localProject ) project.mergin.status = ProjectStatus::projectStatus( project, mBackend->supportsSelectiveSync() ); } - QModelIndex editIndex = index( ix ); + const QModelIndex editIndex = index( ix ); emit dataChanged( editIndex, editIndex ); } -void ProjectsModel::onProjectDetachedFromMergin( const QString &projectFullName ) +void ProjectsModel::onProjectDetachedFromMergin( const QString &projectId ) { - int ix = projectIndexFromId( projectFullName ); + const int ix = projectIndexFromId( projectId ); if ( ix < 0 ) return; @@ -549,21 +584,20 @@ void ProjectsModel::onProjectDetachedFromMergin( const QString &projectFullName project.mergin = MerginProject(); project.local.projectNamespace = QLatin1String(); - QModelIndex editIndex = index( ix ); + const QModelIndex editIndex = index( ix ); emit dataChanged( editIndex, editIndex ); // This project should also be removed from project list for remote project model types, // however, currently one needs to click on "My projects/Shared/Explore" and that sends // another listProjects request. In new list this project will not be shown. - // However, this option is not allowed in GUI anyways. + // However, this option is not allowed in GUI anyway. } -void ProjectsModel::onProjectAttachedToMergin( const QString & ) +void ProjectsModel::onProjectAttachedToMergin( const QString &projectId ) { - // To ensure project will be in sync with server, send listProjectByName request. - // In theory we could send that request only for this one project. - listProjectsByName(); + // To ensure project will be in sync with server, send fetchProjectsByProjectId request. + fetchProjectsByProjectId( { projectId } ); } void ProjectsModel::onAuthChanged() @@ -595,7 +629,7 @@ void ProjectsModel::setLocalProjectsManager( LocalProjectsManager *localProjects emit localProjectsManagerChanged( mLocalProjectsManager ); } -void ProjectsModel::setModelType( ProjectsModel::ProjectModelTypes modelType ) +void ProjectsModel::setModelType( const ProjectsModel::ProjectModelTypes modelType ) { if ( mModelType == modelType ) return; @@ -617,20 +651,6 @@ QString ProjectsModel::modelTypeToFlag() const } } -QStringList ProjectsModel::projectNames() const -{ - QStringList projectNames; - const LocalProjectsList projects = mLocalProjectsManager->projects(); - - for ( const auto &proj : projects ) - { - if ( !proj.projectName.isEmpty() && !proj.projectNamespace.isEmpty() ) - projectNames << proj.id(); - } - - return projectNames; -} - void ProjectsModel::clearProjects() { beginResetModel(); @@ -651,6 +671,26 @@ void ProjectsModel::loadLocalProjects() } } +QStringList ProjectsModel::filterBrokenProjects( const MerginProjectsList &list ) +{ + QStringList errorProjects; + for ( MerginProject project : list ) + { + if ( !project.remoteError.isEmpty() ) + { + const auto res = std::find_if( mProjects.begin(), mProjects.end(), [&project]( const Project & localProject ) + { + return project.fullName() == localProject.fullName(); + } ); + if ( res != mProjects.end() ) + { + errorProjects.append( res->id() ); + } + } + } + return errorProjects; +} + int ProjectsModel::projectIndexFromId( const QString &projectId ) const { for ( int i = 0; i < mProjects.count(); i++ ) @@ -671,12 +711,12 @@ Project ProjectsModel::projectFromId( const QString &projectId ) const return project; } } - return Project(); + return {}; } QModelIndex ProjectsModel::projectModelIndexFromId( const QString &projectId ) const { - int row = projectIndexFromId( projectId ); + const int row = projectIndexFromId( projectId ); return index( row ); } @@ -685,7 +725,7 @@ bool ProjectsModel::isLoading() const return mModelIsLoading; } -void ProjectsModel::setModelIsLoading( bool state ) +void ProjectsModel::setModelIsLoading( const bool state ) { mModelIsLoading = state; emit isLoadingChanged( mModelIsLoading ); diff --git a/app/projectsmodel.h b/app/projectsmodel.h index 3c7542686..257e6d812 100644 --- a/app/projectsmodel.h +++ b/app/projectsmodel.h @@ -11,9 +11,7 @@ #define PROJECTSMODEL_H #include -#include -#include "inputconfig.h" #include "project.h" #include "merginapi.h" #include "synchronizationmanager.h" @@ -21,24 +19,29 @@ class LocalProjectsManager; /** - * \brief The ProjectsModel class holds projects (both local and mergin). Model loads local projects from LocalProjectsManager that hold them - during runtime. Remote (Mergin) projects are fetched from MerginAPI calling listProjects or listProjectsByName (based on the type of the model). + * \brief The ProjectsModel class holds projects (both local and mergin). Model loads local projects from + * LocalProjectsManager that hold them during runtime. Remote (Mergin) projects are fetched from MerginAPI + * calling listProjects or listProjectsByName (based on the type of the model). * - * The main job of the model is to merge projects coming from MerginAPI and LocalProjectsManager. By merging it means Each time new response is received from MerginAPI, model erases - * old remembered projects and fetches new. Merge logic depends on the model type (described below). + * The main job of the model is to merge projects coming from MerginAPI and LocalProjectsManager. By merging it means + * each time new response is received from MerginAPI, model erases old remembered projects and fetches new. + * Merge logic depends on the model type (described below). * * Model can have different types that affect handling of the projects. - * - LocalProjectsModel always keeps all local projects and seek their mergin part when listProjectsByNameFinished - * - Workspace-, and PublicProjectsModel does the opposite, keeps all mergin projects and seeks their local part in projects from LocalProjectsManager + * - LocalProjectsModel always keeps all local projects and seeks their mergin part when listProjectsByNameFinished + * - Workspace-, and PublicProjectsModel does the opposite, keeps all mergin projects and seeks their local part + * in projects from LocalProjectsManager * - EmptyProjectsModel is default state * - * To avoid overriding of requests, model remembers last sent request ID and upon receiving signal from MerginAPI about listProjectsFinished, it firsts compares - * the remembered ID with returned ID. If they do not match, response is ignored. + * To avoid overriding of requests, model remembers last sent request ID and upon receiving signal from MerginAPI + * about listProjectsFinished, it firsts compares the remembered ID with returned ID. If they do not match, response + * is ignored. * * Model also support pagination. To fetch another page call fetchAnotherPage. * - * This is a QML type with 3 required properties (pointer to merginApi, pointer to localProjectsManager and modelType). Without these properties model does nothing. - * After setting all of these properties, model is initialized, starts listening to various signals and offers data. + * This is a QML type with 3 required properties (pointer to merginApi, pointer to localProjectsManager and modelType). + * Without these properties model does nothing. After setting all of these properties, model is initialized, + * starts listening to various signals and offers data. */ class ProjectsModel : public QAbstractListModel { @@ -69,7 +72,8 @@ class ProjectsModel : public QAbstractListModel /** * \brief The ProjectModelTypes enum: * - LocalProjectsModel always keeps all local projects and seek their mergin part when listProjectsByNameFinished - * - Workspace-, and PublicProjectsModel does the opposite, keeps all mergin projects and seeks their local part in projects from LocalProjectsManager + * - Workspace-, and PublicProjectsModel does the opposite, keeps all mergin projects and seeks their local part + * in projects from LocalProjectsManager * - EmptyProjectsModel is default state */ enum ProjectModelTypes @@ -88,10 +92,10 @@ class ProjectsModel : public QAbstractListModel DiscardPrevious }; - ProjectsModel( QObject *parent = nullptr ); - ~ProjectsModel() override {}; + explicit ProjectsModel( QObject *parent = nullptr ); + ~ProjectsModel() override = default; - // From Qt 5.15 we can use REQUIRED keyword here, that will ensure object will be always instantiated from QML with these mandatory properties + // From Qt 5.15 we can use REQUIRED keyword here, that will ensure object will always be instantiated from QML with these mandatory properties Q_PROPERTY( MerginApi *merginApi READ merginApi WRITE setMerginApi NOTIFY merginApiChanged ) Q_PROPERTY( ProjectModelTypes modelType READ modelType WRITE setModelType NOTIFY modelTypeChanged ) Q_PROPERTY( SynchronizationManager *syncManager READ syncManager WRITE setSyncManager NOTIFY syncManagerChanged ) @@ -116,17 +120,21 @@ class ProjectsModel : public QAbstractListModel //! lists projects, either fetch more or get first, search expression Q_INVOKABLE void listProjects( const QString &searchExpression = QString(), int page = 1 ); - //! lists projects via listProjectsByName API, used in LocalProjectsModel - Q_INVOKABLE void listProjectsByName(); + /** + * Lists projects via listProjectsByName API, used in LocalProjectsModel. If projectIds is empty fetches all local + * projects else fetches only specified projects. + * \param projectIds List of project IDs to fetch, by default empty + */ + Q_INVOKABLE void fetchProjectsByProjectId( const QStringList &projectIds = QStringList() ); //! Syncs specified project - upload or update Q_INVOKABLE void syncProject( const QString &projectId ); //! Stops running project upload or update - Q_INVOKABLE void stopProjectSync( const QString &projectId ); + Q_INVOKABLE void stopProjectSync( const QString &projectId ) const; //! Forwards call to LocalProjectsManager to remove local project - Q_INVOKABLE void removeLocalProject( const QString &projectId ); + Q_INVOKABLE void removeLocalProject( const QString &projectId ) const; //! Migrates local project to mergin Q_INVOKABLE void migrateProject( const QString &projectId ); @@ -155,22 +163,23 @@ class ProjectsModel : public QAbstractListModel public slots: // MerginAPI - project list signals - void onListProjectsFinished( const MerginProjectsList &merginProjects, int projectsCount, int page, QString requestId ); - void onListProjectsByNameFinished( const MerginProjectsList &merginProjects, QString requestId ); + void onListProjectsFinished( const MerginProjectsList &merginProjects, int projectsCount, int page, const QString &requestId ); + void onListProjectsByNameFinished( const MerginProjectsList &merginProjects, const QString &requestId ); + void onRefetchBrokenProjectsFinished( const MerginProjectsList &merginProjects ); - // Synchonization signals - void onProjectSyncStarted( const QString &projectFullName ); - void onProjectSyncCancelled( const QString &projectFullName ); - void onProjectSyncProgressChanged( const QString &projectFullName, qreal progress ); - void onProjectSyncFinished( const QString &projectFullName, bool successfully, int newVersion ); + // Synchronization signals + void onProjectSyncStarted( const QString &projectId ); + void onProjectSyncCancelled( const QString &projectId ); + void onProjectSyncProgressChanged( const QString &projectId, qreal progress ); + void onProjectSyncFinished( const QString &projectId, bool successfully, int newVersion ); - void onProjectDetachedFromMergin( const QString &projectFullName ); - void onProjectAttachedToMergin( const QString &projectFullName ); + void onProjectDetachedFromMergin( const QString &projectId ); + void onProjectAttachedToMergin( const QString &projectId ); // LocalProjectsManager signals - void onProjectAdded( const LocalProject &project ); - void onAboutToRemoveProject( const LocalProject &project ); - void onProjectDataChanged( const LocalProject &project ); + void onProjectAdded( const LocalProject &localProject ); + void onAboutToRemoveProject( const LocalProject &localProject ); + void onProjectDataChanged( const LocalProject &localProject ); void onAuthChanged(); @@ -195,13 +204,12 @@ class ProjectsModel : public QAbstractListModel void activeProjectIdChanged( QString projectId ); private: - + QStringList filterBrokenProjects( const MerginProjectsList &list ); int projectIndexFromId( const QString &projectId ) const; void setModelIsLoading( bool state ); QString modelTypeToFlag() const; - QStringList projectNames() const; void clearProjects(); void loadLocalProjects(); void initializeProjectsModel(); diff --git a/app/projectsproxymodel.cpp b/app/projectsproxymodel.cpp index 9f30d84b5..a5cc44a1f 100644 --- a/app/projectsproxymodel.cpp +++ b/app/projectsproxymodel.cpp @@ -37,7 +37,7 @@ ProjectsModel *ProjectsProxyModel::projectSourceModel() const return mModel; } -void ProjectsProxyModel::setActiveProjectAlwaysFirst( bool value ) +void ProjectsProxyModel::setActiveProjectAlwaysFirst( const bool value ) { mActiveProjectAlwaysFirst = value; invalidate(); @@ -48,7 +48,7 @@ bool ProjectsProxyModel::activeProjectAlwaysFirst() const return mActiveProjectAlwaysFirst; } -void ProjectsProxyModel::setSearchExpression( QString searchExpression ) +void ProjectsProxyModel::setSearchExpression( const QString &searchExpression ) { if ( mSearchExpression == searchExpression ) return; @@ -64,7 +64,7 @@ void ProjectsProxyModel::setProjectSourceModel( ProjectsModel *sourceModel ) return; mModel = sourceModel; - QObject::connect( mModel, &ProjectsModel::modelInitialized, this, &ProjectsProxyModel::initialize ); + connect( mModel, &ProjectsModel::modelInitialized, this, &ProjectsProxyModel::initialize ); } bool ProjectsProxyModel::lessThan( const QModelIndex &left, const QModelIndex &right ) const @@ -73,14 +73,14 @@ bool ProjectsProxyModel::lessThan( const QModelIndex &left, const QModelIndex &r { if ( mActiveProjectAlwaysFirst ) { - bool lProjectIsActive = mModel->data( left, ProjectsModel::ProjectIsActiveProject ).toBool(); - bool rProjectIsActive = mModel->data( right, ProjectsModel::ProjectIsActiveProject ).toBool(); + const bool lProjectIsActive = mModel->data( left, ProjectsModel::ProjectIsActiveProject ).toBool(); + const bool rProjectIsActive = mModel->data( right, ProjectsModel::ProjectIsActiveProject ).toBool(); if ( lProjectIsActive || rProjectIsActive ) return lProjectIsActive; } - bool lProjectIsMergin = mModel->data( left, ProjectsModel::ProjectIsMergin ).toBool(); - bool rProjectIsMergin = mModel->data( right, ProjectsModel::ProjectIsMergin ).toBool(); + const bool lProjectIsMergin = mModel->data( left, ProjectsModel::ProjectIsMergin ).toBool(); + const bool rProjectIsMergin = mModel->data( right, ProjectsModel::ProjectIsMergin ).toBool(); /** * Ordering of local projects: first non-mergin projects (using folder name), @@ -89,8 +89,8 @@ bool ProjectsProxyModel::lessThan( const QModelIndex &left, const QModelIndex &r if ( !lProjectIsMergin && !rProjectIsMergin ) { - QString lProjectFullName = mModel->data( left, ProjectsModel::ProjectFullName ).toString(); - QString rProjectFullName = mModel->data( right, ProjectsModel::ProjectFullName ).toString(); + const QString lProjectFullName = mModel->data( left, ProjectsModel::ProjectFullName ).toString(); + const QString rProjectFullName = mModel->data( right, ProjectsModel::ProjectFullName ).toString(); return lProjectFullName.compare( rProjectFullName, Qt::CaseInsensitive ) < 0; } @@ -105,10 +105,10 @@ bool ProjectsProxyModel::lessThan( const QModelIndex &left, const QModelIndex &r } // comparing 2 mergin projects - QString lNamespace = mModel->data( left, ProjectsModel::ProjectNamespace ).toString(); - QString lProjectName = mModel->data( left, ProjectsModel::ProjectName ).toString(); - QString rNamespace = mModel->data( right, ProjectsModel::ProjectNamespace ).toString(); - QString rProjectName = mModel->data( right, ProjectsModel::ProjectName ).toString(); + const QString lNamespace = mModel->data( left, ProjectsModel::ProjectNamespace ).toString(); + const QString lProjectName = mModel->data( left, ProjectsModel::ProjectName ).toString(); + const QString rNamespace = mModel->data( right, ProjectsModel::ProjectNamespace ).toString(); + const QString rProjectName = mModel->data( right, ProjectsModel::ProjectName ).toString(); if ( lNamespace == rNamespace ) { diff --git a/app/projectsproxymodel.h b/app/projectsproxymodel.h index 5e7c888d5..bafa6dc42 100644 --- a/app/projectsproxymodel.h +++ b/app/projectsproxymodel.h @@ -10,17 +10,16 @@ #ifndef PROJECTSPROXYMODEL_H #define PROJECTSPROXYMODEL_H -#include #include -#include "inputconfig.h" #include "projectsmodel.h" /** * \brief The ProjectsProxyModel class used as a proxy filter/sort model for the \see ProjectsModel class. * - * ProjectsProxyModel is a QML type with required property of projectSourceModel. Without source model, this model does nothing (is not initialized). - * After setting source model, this model starts sorting and allows filtering (search) from view. + * ProjectsProxyModel is a QML type with required property of projectSourceModel. Without source model, this model does + * nothing (is not initialized). After setting source model, this model starts sorting and allows filtering (search) + * from view. */ class ProjectsProxyModel : public QSortFilterProxyModel { @@ -42,7 +41,7 @@ class ProjectsProxyModel : public QSortFilterProxyModel bool activeProjectAlwaysFirst() const; public slots: - void setSearchExpression( QString searchExpression ); + void setSearchExpression( const QString &searchExpression ); void setProjectSourceModel( ProjectsModel *sourceModel ); signals: diff --git a/app/projectwizard.cpp b/app/projectwizard.cpp index 184ba78ac..a8e80f75d 100644 --- a/app/projectwizard.cpp +++ b/app/projectwizard.cpp @@ -15,17 +15,13 @@ #include "qgsvectortilelayer.h" #include "qgsvectorlayer.h" #include "qgsvectorfilewriter.h" -#include "qgsdatetimefieldformatter.h" #include "qgsmarkersymbollayer.h" -#include "qgis.h" #include "qgslinesymbol.h" #include "qgssymbollayer.h" #include "qgssymbollayerutils.h" #include "qgssymbol.h" #include "qgsmarkersymbol.h" #include "qgssinglesymbolrenderer.h" -#include "inpututils.h" -#include "coreutils.h" const QString TILES_URL = QStringLiteral( "https://tiles.merginmaps.com" ); @@ -34,15 +30,15 @@ ProjectWizard::ProjectWizard( const QString &dataDir, QObject *parent ) , mDataDir( dataDir ) { - mSettings = std::unique_ptr( new QgsMapSettings ); + mSettings = std::make_unique(); } QgsVectorLayer *ProjectWizard::createGpkgLayer( QString const &projectDir, QList const &fieldsConfig ) { - QString gpkgName( QStringLiteral( "data" ) ); - QString projectGpkgPath( QString( "%1/%2.%3" ).arg( projectDir ).arg( gpkgName ).arg( "gpkg" ) ); - QString layerName( QStringLiteral( "Survey" ) ); - QgsCoordinateReferenceSystem layerCrs( LAYER_CRS_ID ); + const QString gpkgName( QStringLiteral( "data" ) ); + const QString projectGpkgPath( QString( "%1/%2.%3" ).arg( projectDir ).arg( gpkgName ).arg( "gpkg" ) ); + const QString layerName( QStringLiteral( "Survey" ) ); + const QgsCoordinateReferenceSystem layerCrs( LAYER_CRS_ID ); QgsFields predefinedFields = createFields( fieldsConfig ); // Write layer as gpkg @@ -94,28 +90,28 @@ static QgsVectorLayer *createTrackingLayer( const QString &trackingGpkgPath ) // (create_tracking_layer(), setup_tracking_layer(), set_tracking_layer_flags()) QgsFields fields; - fields.append( QgsField( "tracking_start_time", QVariant::DateTime ) ); - fields.append( QgsField( "tracking_end_time", QVariant::DateTime ) ); - fields.append( QgsField( "total_distance", QVariant::Double ) ); - fields.append( QgsField( "tracked_by", QVariant::String ) ); + fields.append( QgsField( "tracking_start_time", QMetaType::QDateTime ) ); + fields.append( QgsField( "tracking_end_time", QMetaType::QDateTime ) ); + fields.append( QgsField( "total_distance", QMetaType::Double ) ); + fields.append( QgsField( "tracked_by", QMetaType::QString ) ); QgsVectorFileWriter::SaveVectorOptions options; options.driverName = "GPKG"; options.layerName = "tracking_layer"; - QgsVectorFileWriter *writer = QgsVectorFileWriter::create( - trackingGpkgPath, - fields, - Qgis::WkbType::LineStringZM, - QgsCoordinateReferenceSystem( "EPSG:4326" ), - QgsCoordinateTransformContext(), - options ); + const QgsVectorFileWriter *writer = QgsVectorFileWriter::create( + trackingGpkgPath, + fields, + Qgis::WkbType::LineStringZM, + QgsCoordinateReferenceSystem( "EPSG:4326" ), + QgsCoordinateTransformContext(), + options ); delete writer; QgsVectorLayer *layer = new QgsVectorLayer( trackingGpkgPath, "tracking_layer", "ogr" ); int idx = layer->fields().indexFromName( "fid" ); - QgsEditorWidgetSetup cfg( "Hidden", QVariantMap() ); + const QgsEditorWidgetSetup cfg( "Hidden", QVariantMap() ); layer->setEditorWidgetSetup( idx, cfg ); idx = layer->fields().indexFromName( "tracking_start_time" ); @@ -154,7 +150,7 @@ static QgsVectorLayer *createTrackingLayer( const QString &trackingGpkgPath ) return layer; } -void ProjectWizard::createProject( QString const &projectName, FieldsModel *fieldsModel ) +void ProjectWizard::createProject( QString const &projectName, const FieldsModel *fieldsModel ) { if ( !CoreUtils::isValidName( projectName ) ) { @@ -162,10 +158,10 @@ void ProjectWizard::createProject( QString const &projectName, FieldsModel *fiel return; } - QString projectDir = CoreUtils::createUniqueProjectDirectory( mDataDir, projectName ); - QString projectFilepath( QString( "%1/%2.qgz" ).arg( projectDir ).arg( projectName ) ); - QString projectGpkgPath( QString( "%1/data.gpkg" ).arg( projectDir ) ); - QString trackingGpkgPath( QString( "%1/tracking_layer.gpkg" ).arg( projectDir ) ); + const QString projectDir = CoreUtils::createUniqueProjectDirectory( mDataDir, projectName ); + const QString projectFilepath( QString( "%1/%2.qgz" ).arg( projectDir ).arg( projectName ) ); + const QString projectGpkgPath( QString( "%1/data.gpkg" ).arg( projectDir ) ); + const QString trackingGpkgPath( QString( "%1/tracking_layer.gpkg" ).arg( projectDir ) ); QgsProject project; @@ -194,7 +190,7 @@ void ProjectWizard::createProject( QString const &projectName, FieldsModel *fiel project.addMapLayers( layers ); // Configurate mapSettings - QgsCoordinateReferenceSystem projectCrs( PROJECT_CRS_ID ); + const QgsCoordinateReferenceSystem projectCrs( PROJECT_CRS_ID ); mSettings->setExtent( bgLayer->extent() ); mSettings->setEllipsoid( "WGS84" ); mSettings->setDestinationCrs( projectCrs ); @@ -213,9 +209,9 @@ void ProjectWizard::createProject( QString const &projectName, FieldsModel *fiel emit projectCreated( projectDir, projectName ); } -void ProjectWizard::writeMapCanvasSetting( QDomDocument &doc ) +void ProjectWizard::writeMapCanvasSetting( QDomDocument &doc ) const { - QDomNodeList nl = doc.elementsByTagName( QStringLiteral( "qgis" ) ); + const QDomNodeList nl = doc.elementsByTagName( QStringLiteral( "qgis" ) ); if ( !nl.count() ) { QgsDebugError( QStringLiteral( "Unable to find qgis element in project file" ) ); @@ -229,14 +225,14 @@ void ProjectWizard::writeMapCanvasSetting( QDomDocument &doc ) mSettings->writeXml( mapcanvasNode, doc ); } -QgsFields ProjectWizard::createFields( const QList fieldsConfig ) const +QgsFields ProjectWizard::createFields( const QList &fieldsConfig ) const { QgsFields fields; for ( const FieldConfiguration &fc : fieldsConfig ) { QString type = widgetToType( fc.widgetType ); - QVariant::Type qtype = parseType( type ); + QMetaType::Type qtype = parseType( type ); QgsField field( fc.attributeName, qtype, type ); fields.append( field ); } @@ -254,45 +250,45 @@ QgsSingleSymbolRenderer *ProjectWizard::surveyLayerRenderer() return new QgsSingleSymbolRenderer( symbol ); } -QVariant::Type ProjectWizard::parseType( const QString &type ) const +QMetaType::Type ProjectWizard::parseType( const QString &type ) const { if ( type == QLatin1String( "text" ) ) - return QVariant::String; - else if ( type == QLatin1String( "integer" ) ) - return QVariant::Int; - else if ( type == QLatin1String( "integer64" ) ) - return QVariant::Int; - else if ( type == QLatin1String( "real" ) ) - return QVariant::Double; - else if ( type == QLatin1String( "date" ) ) - return QVariant::Date; - else if ( type == QLatin1String( "datetime" ) ) - return QVariant::DateTime; - else if ( type == QLatin1String( "bool" ) ) - return QVariant::Bool; - else if ( type == QLatin1String( "binary" ) ) - return QVariant::ByteArray; - - return QVariant::Invalid; + return QMetaType::QString; + if ( type == QLatin1String( "integer" ) ) + return QMetaType::Int; + if ( type == QLatin1String( "integer64" ) ) + return QMetaType::Int; + if ( type == QLatin1String( "real" ) ) + return QMetaType::Double; + if ( type == QLatin1String( "date" ) ) + return QMetaType::QDate; + if ( type == QLatin1String( "datetime" ) ) + return QMetaType::QDateTime; + if ( type == QLatin1String( "bool" ) ) + return QMetaType::Bool; + if ( type == QLatin1String( "binary" ) ) + return QMetaType::QByteArray; + + return QMetaType::UnknownType; } QString ProjectWizard::widgetToType( const QString &widgetType ) const { if ( widgetType == QStringLiteral( "TextEdit" ) ) return QStringLiteral( "text" ); - else if ( widgetType == QStringLiteral( "Range" ) ) + if ( widgetType == QStringLiteral( "Range" ) ) return QStringLiteral( "integer" ); - else if ( widgetType == QStringLiteral( "DateTime" ) ) + if ( widgetType == QStringLiteral( "DateTime" ) ) return QStringLiteral( "datetime" ); - else if ( widgetType == QStringLiteral( "CheckBox" ) ) + if ( widgetType == QStringLiteral( "CheckBox" ) ) return QStringLiteral( "bool" ); - else if ( widgetType == QStringLiteral( "ExternalResource" ) ) + if ( widgetType == QStringLiteral( "ExternalResource" ) ) return QStringLiteral( "text" ); return QStringLiteral( "text" ); } -QString ProjectWizard::findWidgetTypeByFieldName( const QString name, const QList fieldsConfig ) const +QString ProjectWizard::findWidgetTypeByFieldName( const QString &name, const QList &fieldsConfig ) const { for ( int i = 0; i < fieldsConfig.count(); ++i ) diff --git a/app/projectwizard.h b/app/projectwizard.h index 6dcc20deb..41b7fc87c 100644 --- a/app/projectwizard.h +++ b/app/projectwizard.h @@ -10,50 +10,47 @@ #ifndef PROJECTWIZARD_H #define PROJECTWIZARD_H -#include - -#include "inputconfig.h" #include "fieldsmodel.h" #include "qgsfieldmodel.h" #include "qgsvectorlayer.h" #include "qgsmapsettings.h" -#include /** - * Controller for creating new Input project. + * Controller for creating new Mergin Maps project. */ class ProjectWizard : public QObject { Q_OBJECT public: explicit ProjectWizard( const QString &dataDir, QObject *parent = nullptr ); - ~ProjectWizard() = default; + ~ProjectWizard() override = default; /** - * Creates a new project in unique directory named accoridng project name. + * Creates a new project in unique directory named according to project name. * \param projectName Project's name for newly created project. * \param fieldsModel Fields configuration model for a new project */ - Q_INVOKABLE void createProject( QString const &projectName, FieldsModel *fieldsModel ); + Q_INVOKABLE void createProject( QString const &projectName, const FieldsModel *fieldsModel ); public slots: //! To append "mapcanvas" property to project file required to correctly show a project. - void writeMapCanvasSetting( QDomDocument &doc ); + void writeMapCanvasSetting( QDomDocument &doc ) const; signals: /** - * Emitted after a project has been craeted. + * Emitted after a project has been created. */ void projectCreationFailed( const QString &message ); void projectCreated( const QString &projectDir, const QString &projectName ); void notifySuccess( const QString &message ); private: QgsVectorLayer *createGpkgLayer( QString const &projectDir, QList const &fieldsConfig ); - QgsFields createFields( const QList fieldsConfig ) const; + QgsFields createFields( const QList &fieldsConfig ) const; QgsSingleSymbolRenderer *surveyLayerRenderer(); - QVariant::Type parseType( const QString &type ) const; + + QMetaType::Type parseType( const QString &type ) const; QString widgetToType( const QString &widgetType ) const; - QString findWidgetTypeByFieldName( const QString name, const QList fieldsConfig ) const; + QString findWidgetTypeByFieldName( const QString &name, const QList &fieldsConfig ) const; QString mDataDir; std::unique_ptr mSettings = nullptr; diff --git a/app/qml/main.qml b/app/qml/main.qml index 2cfbd37d5..71cb44f32 100644 --- a/app/qml/main.qml +++ b/app/qml/main.qml @@ -907,17 +907,17 @@ ApplicationWindow { target: __syncManager enabled: stateManager.state === "map" - function onSyncStarted( projectFullName ) + function onSyncStarted( projectId ) { - if ( projectFullName === __activeProject.projectFullName() ) + if ( projectId === __activeProject.projectId() ) { syncButton.iconRotateAnimationRunning = true } } - function onSyncFinished( projectFullName, success ) + function onSyncFinished( projectId, success ) { - if ( projectFullName === __activeProject.projectFullName() ) + if ( projectId === __activeProject.projectId() ) { syncButton.iconRotateAnimationRunning = false @@ -931,17 +931,17 @@ ApplicationWindow { } } - function onSyncCancelled( projectFullName ) + function onSyncCancelled( projectId ) { - if ( projectFullName === __activeProject.projectFullName() ) + if ( projectId === __activeProject.projectId() ) { syncButton.iconRotateAnimationRunning = false } } - function onSyncError( projectFullName, errorType, willRetry, errorMessage ) + function onSyncError( projectId, errorType, willRetry, errorMessage ) { - if ( projectFullName === __activeProject.projectFullName() ) + if ( projectId === __activeProject.projectId() ) { if ( errorType === MM.SyncError.NotAMerginProject ) { @@ -975,10 +975,10 @@ ApplicationWindow { Connections { target: __merginApi - function onNetworkErrorOccurred( message, topic, httpCode, projectFullName ) { + function onNetworkErrorOccurred( message, httpCode, _ ) { if ( stateManager.state === "projects" ) { - var msg = message ? message : qsTr( "Failed to communicate with server. Try improving your network connection." ) + const msg = message ? message : qsTr("Failed to communicate with server. Try improving your network connection."); __notificationModel.addError( msg ) } } @@ -1003,9 +1003,9 @@ ApplicationWindow { syncButton.iconRotateAnimationRunning = false } - function onProjectDataChanged( projectFullName ) { + function onProjectDataChanged( _, projectId ) { //! if current project has been updated, refresh canvas - if ( projectFullName === projectController.activeProjectId ) { + if ( projectId === projectController.activeProjectId ) { map.mapSettings.extentChanged() } } @@ -1017,17 +1017,17 @@ ApplicationWindow { } } - function onMissingAuthorizationError( projectFullName ) + function onMissingAuthorizationError( projectId ) { - if ( projectFullName === __activeProject.projectFullName() && !__merginApi.userAuth.isUsingSso() ) + if ( projectId === __activeProject.projectId() && !__merginApi.userAuth.isUsingSso() ) { missingAuthDialog.open() } } - function onProjectAlreadyOnLatestVersion( projectFullName ) + function onProjectAlreadyOnLatestVersion( projectId ) { - if ( projectFullName === __activeProject.projectFullName() ) + if ( projectId === __activeProject.projectId() ) { __notificationModel.addSuccess( qsTr( "Up to date" ) ) } diff --git a/app/synchronizationmanager.cpp b/app/synchronizationmanager.cpp index 85888dd1d..5e9376419 100644 --- a/app/synchronizationmanager.cpp +++ b/app/synchronizationmanager.cpp @@ -11,6 +11,8 @@ #include "synchronizationmanager.h" +#include "synchronizationerror.h" + SynchronizationManager::SynchronizationManager( MerginApi *merginApi, QObject *parent @@ -20,19 +22,18 @@ SynchronizationManager::SynchronizationManager( { if ( mMerginApi ) { - QObject::connect( mMerginApi, &MerginApi::pushCanceled, this, &SynchronizationManager::onProjectSyncCanceled ); - QObject::connect( mMerginApi, &MerginApi::syncProjectFinished, this, &SynchronizationManager::onProjectSyncFinished ); - QObject::connect( mMerginApi, &MerginApi::networkErrorOccurred, this, &SynchronizationManager::onProjectSyncFailure ); - QObject::connect( mMerginApi, &MerginApi::projectCreated, this, &SynchronizationManager::onProjectCreated ); - QObject::connect( mMerginApi, &MerginApi::projectAttachedToMergin, this, &SynchronizationManager::onProjectAttachedToMergin ); - QObject::connect( mMerginApi, &MerginApi::syncProjectStatusChanged, this, &SynchronizationManager::onProjectSyncProgressChanged ); - QObject::connect( mMerginApi, &MerginApi::projectReloadNeededAfterSync, this, &SynchronizationManager::onProjectReloadNeededAfterSync ); + connect( mMerginApi, &MerginApi::pushCanceled, this, &SynchronizationManager::onProjectSyncCanceled ); + connect( mMerginApi, &MerginApi::syncProjectFinished, this, &SynchronizationManager::onProjectSyncFinished ); + connect( mMerginApi, &MerginApi::networkErrorOccurred, this, &SynchronizationManager::onProjectSyncFailure ); + connect( mMerginApi, &MerginApi::projectCreated, this, &SynchronizationManager::onProjectCreated ); + connect( mMerginApi, &MerginApi::syncProjectStatusChanged, this, &SynchronizationManager::onProjectSyncProgressChanged ); + connect( mMerginApi, &MerginApi::projectReloadNeededAfterSync, this, &SynchronizationManager::onProjectReloadNeededAfterSync ); } } SynchronizationManager::~SynchronizationManager() = default; -void SynchronizationManager::syncProject( const Project &project, SyncOptions::Authorization auth, SyncOptions::Strategy strategy ) +void SynchronizationManager::syncProject( const Project &project, const SyncOptions::Authorization auth, const SyncOptions::Strategy strategy ) { if ( project.isLocal() ) { @@ -41,19 +42,19 @@ void SynchronizationManager::syncProject( const Project &project, SyncOptions::A } // project is not local yet -> we download it for the first time - bool syncHasStarted = mMerginApi->pullProject( project.mergin.projectNamespace, project.mergin.projectName, auth == SyncOptions::Authorized ); + const bool syncHasStarted = mMerginApi->pullProject( project.fullName(), project.id(), auth == SyncOptions::Authorized ); if ( syncHasStarted ) { - SyncProcess &process = mSyncProcesses[project.fullName()]; // gets or creates + SyncProcess &process = mSyncProcesses[project.id()]; // gets or creates process.pending = true; process.strategy = strategy; - emit syncStarted( project.fullName() ); + emit syncStarted( project.id() ); } } -void SynchronizationManager::syncProject( const LocalProject &project, SyncOptions::Authorization auth, SyncOptions::Strategy strategy ) +void SynchronizationManager::syncProject( const LocalProject &project, const SyncOptions::Authorization auth, const SyncOptions::Strategy strategy ) { if ( !project.isValid() ) { @@ -66,16 +67,14 @@ void SynchronizationManager::syncProject( const LocalProject &project, SyncOptio return; } - QString projectFullName = MerginApi::getFullProjectName( project.projectNamespace, project.projectName ); - - if ( mSyncProcesses.contains( projectFullName ) ) + if ( mSyncProcesses.contains( project.id() ) ) { - SyncProcess &process = mSyncProcesses[projectFullName]; + SyncProcess &process = mSyncProcesses[project.id()]; if ( process.pending ) { return; // this project is currently syncing } - else if ( process.awaitsRetry ) + if ( process.awaitsRetry ) { process.awaitsRetry = false; } @@ -85,85 +84,85 @@ void SynchronizationManager::syncProject( const LocalProject &project, SyncOptio if ( ProjectStatus::hasLocalChanges( project, mMerginApi->supportsSelectiveSync() ) ) { - syncHasStarted = mMerginApi->pushProject( project.projectNamespace, project.projectName ); + syncHasStarted = mMerginApi->pushProject( project.fullName(), project.id() ); } else { - syncHasStarted = mMerginApi->pullProject( project.projectNamespace, project.projectName, auth == SyncOptions::Authorized ); + syncHasStarted = mMerginApi->pullProject( project.fullName(), project.id(), auth == SyncOptions::Authorized ); } if ( syncHasStarted ) { - SyncProcess &process = mSyncProcesses[projectFullName]; // gets or creates + SyncProcess &process = mSyncProcesses[project.id()]; // gets or creates process.pending = true; process.strategy = strategy; - emit syncStarted( projectFullName ); + emit syncStarted( project.id() ); } } -void SynchronizationManager::stopProjectSync( const QString &projectFullname ) +void SynchronizationManager::stopProjectSync( const QString &projectId ) const { - if ( mSyncProcesses.contains( projectFullname ) ) + if ( mSyncProcesses.contains( projectId ) ) { Transactions syncTransactions = mMerginApi->transactions(); - if ( syncTransactions.contains( projectFullname ) ) + if ( syncTransactions.contains( projectId ) ) { - TransactionStatus &transaction = syncTransactions[projectFullname]; + const TransactionStatus &transaction = syncTransactions[projectId]; if ( transaction.type == TransactionStatus::Pull ) { - mMerginApi->cancelPull( projectFullname ); + mMerginApi->cancelPull( projectId ); } else { - mMerginApi->cancelPush( projectFullname ); + mMerginApi->cancelPush( projectId ); } } } } -void SynchronizationManager::migrateProjectToMergin( const QString &projectName ) +void SynchronizationManager::migrateProjectToMergin( const QString &projectName, const QString &projectId ) { - if ( !mSyncProcesses.contains( projectName ) ) + if ( !mSyncProcesses.contains( projectId ) ) { bool hasStarted = false; if ( mMerginApi->serverType() == MerginServerType::OLD ) { - hasStarted = mMerginApi->createProject( mMerginApi->userInfo()->username(), projectName ); + hasStarted = mMerginApi->createProject( mMerginApi->userInfo()->username(), projectName, projectId ); } else { - hasStarted = mMerginApi->createProject( mMerginApi->userInfo()->activeWorkspaceName(), projectName ); + hasStarted = mMerginApi->createProject( mMerginApi->userInfo()->activeWorkspaceName(), projectName, projectId ); } if ( hasStarted ) { - SyncProcess &process = mSyncProcesses[projectName]; // creates new entry + SyncProcess &process = mSyncProcesses[projectId]; // creates new entry process.pending = true; - emit syncStarted( projectName ); + emit syncStarted( projectId ); } } } -qreal SynchronizationManager::syncProgress( const QString &projectFullName ) const +qreal SynchronizationManager::syncProgress( const QString &projectId ) const { - if ( mSyncProcesses.contains( projectFullName ) ) + if ( mSyncProcesses.contains( projectId ) ) { - return mSyncProcesses.value( projectFullName ).progress; + return mSyncProcesses.value( projectId ).progress; } return -1; } -bool SynchronizationManager::hasPendingSync( const QString &projectFullName ) const +bool SynchronizationManager::hasPendingSync( const QString &projectId ) const { - if ( mSyncProcesses.contains( projectFullName ) ) + if ( mSyncProcesses.contains( projectId ) ) { - return mSyncProcesses.value( projectFullName ).pending; + return mSyncProcesses.value( projectId ).pending; } return false; @@ -174,23 +173,21 @@ QList SynchronizationManager::pendingProjects() const return mSyncProcesses.keys(); } -void SynchronizationManager::onProjectSyncCanceled( const QString &projectFullName, bool withError ) +void SynchronizationManager::onProjectSyncCanceled( const QString &projectId ) { - Q_UNUSED( withError ) - - if ( mSyncProcesses.contains( projectFullName ) ) + if ( mSyncProcesses.contains( projectId ) ) { - mSyncProcesses.remove( projectFullName ); - emit syncCancelled( projectFullName ); + mSyncProcesses.remove( projectId ); + emit syncCancelled( projectId ); } } -void SynchronizationManager::onProjectSyncFinished( const QString &projectFullName, bool successfully, int version ) +void SynchronizationManager::onProjectSyncFinished( const QString &projectId, const bool successfully, const int version ) { - if ( mSyncProcesses.contains( projectFullName ) ) + if ( mSyncProcesses.contains( projectId ) ) { - SyncProcess &process = mSyncProcesses[projectFullName]; - bool reloadNeeded = process.reloadProject; + SyncProcess &process = mSyncProcesses[projectId]; + const bool reloadNeeded = process.reloadProject; if ( !successfully && process.awaitsRetry ) { @@ -200,114 +197,94 @@ void SynchronizationManager::onProjectSyncFinished( const QString &projectFullNa } else // successfully or we won't try again { - mSyncProcesses.remove( projectFullName ); + mSyncProcesses.remove( projectId ); } - emit syncFinished( projectFullName, successfully, version, reloadNeeded ); + emit syncFinished( projectId, successfully, version, reloadNeeded ); } } -void SynchronizationManager::onProjectSyncProgressChanged( const QString &projectFullName, qreal progress ) +void SynchronizationManager::onProjectSyncProgressChanged( const QString &projectId, const qreal progress ) { - if ( mSyncProcesses.contains( projectFullName ) ) + if ( mSyncProcesses.contains( projectId ) ) { - mSyncProcesses[projectFullName].progress = progress; - emit syncProgressChanged( projectFullName, progress ); + mSyncProcesses[projectId].progress = progress; + emit syncProgressChanged( projectId, progress ); } else if ( progress >= 0 ) { // // Synchronization was not started via sync manager, // let's add it to the manager here. - // This is most probably usefull only for tests, where we + // This is most probably useful only for tests, where we // normally run sync from MerginApi directly // - SyncProcess &process = mSyncProcesses[projectFullName]; + SyncProcess &process = mSyncProcesses[projectId]; process.pending = true; process.progress = progress; - emit syncStarted( projectFullName ); - emit syncProgressChanged( projectFullName, progress ); + emit syncStarted( projectId ); + emit syncProgressChanged( projectId, progress ); } } -void SynchronizationManager::onProjectCreated( const QString &projectFullName, bool result ) +void SynchronizationManager::onProjectCreated( const QString &projectId, const bool result ) { - // 'projectFullName' is in the format "namespace/projectName" and 'mSyncProcess' stores - // projects that were not previously uploaded to the server in the format "projectName". - QString projectNamespace, projectName; - MerginApi::extractProjectName( projectFullName, projectNamespace, projectName ); - - if ( !result && mSyncProcesses.contains( projectName ) ) + if ( !result && mSyncProcesses.contains( projectId ) ) { - mSyncProcesses.remove( projectName ); + mSyncProcesses.remove( projectId ); } } void SynchronizationManager::onProjectSyncFailure( const QString &message, - const QString &topic, - int errorCode, - const QString &projectFullName ) + const int httpCode, + const QString &projectId ) { - if ( projectFullName.isEmpty() ) + if ( projectId.isEmpty() ) { return; // network error outside of sync } - if ( !mSyncProcesses.contains( projectFullName ) ) + if ( !mSyncProcesses.contains( projectId ) ) { return; } - Q_UNUSED( topic ); - - SyncProcess &process = mSyncProcesses[projectFullName]; + SyncProcess &process = mSyncProcesses[projectId]; - SynchronizationError::ErrorType error = SynchronizationError::errorType( errorCode, message ); + const SynchronizationError::ErrorType error = SynchronizationError::errorType( httpCode, message ); // We only retry twice - bool eligibleForRetry = process.strategy == SyncOptions::Retry && - process.retriesCount < 2 && - !SynchronizationError::isPermanent( error ); + const bool eligibleForRetry = process.strategy == SyncOptions::Retry && + process.retriesCount < 2 && + !SynchronizationError::isPermanent( error ); - emit syncError( projectFullName, error, eligibleForRetry, message ); + emit syncError( projectId, error, eligibleForRetry, message ); if ( eligibleForRetry ) { process.retriesCount = process.retriesCount + 1; process.awaitsRetry = true; - QTimer::singleShot( mSyncRetryIntervalSeconds, this, [this, projectFullName]() + QTimer::singleShot( mSyncRetryIntervalSeconds, this, [this, projectId] { - LocalProject project = mMerginApi->getLocalProject( projectFullName ); + const LocalProject project = mMerginApi->getLocalProject( projectId ); syncProject( project ); } ); } else { - mSyncProcesses.remove( projectFullName ); - emit syncFinished( projectFullName, false, -1, false ); - - return; - } -} - -void SynchronizationManager::onProjectAttachedToMergin( const QString &projectFullName, const QString &previousName ) -{ - if ( mSyncProcesses.contains( previousName ) ) - { - SyncProcess process = mSyncProcesses.value( previousName ); - mSyncProcesses.remove( previousName ); - mSyncProcesses.insert( projectFullName, process ); + mSyncProcesses.remove( projectId ); + emit syncFinished( projectId, false, -1, false ); } } -void SynchronizationManager::onProjectReloadNeededAfterSync( const QString &projectFullName ) +void SynchronizationManager::onProjectReloadNeededAfterSync( const QString &projectId ) { - if ( mSyncProcesses.contains( projectFullName ) ) + if ( mSyncProcesses.contains( projectId ) ) { - mSyncProcesses[projectFullName].reloadProject = true; + mSyncProcesses[projectId].reloadProject = true; } } diff --git a/app/synchronizationmanager.h b/app/synchronizationmanager.h index ba3d02448..1063300ac 100644 --- a/app/synchronizationmanager.h +++ b/app/synchronizationmanager.h @@ -11,12 +11,9 @@ #define SYNCHRONIZATIONMANAGER_H #include -#include -#include "inputconfig.h" #include "project.h" #include "merginapi.h" -#include "synchronizationerror.h" #include "synchronizationoptions.h" struct SyncProcess @@ -40,30 +37,30 @@ class SynchronizationManager : public QObject explicit SynchronizationManager( MerginApi *merginApi, QObject *parent = nullptr ); - virtual ~SynchronizationManager(); + ~SynchronizationManager() override; - //! Stops a running sync process if there is one for project specified by projectFullname - void stopProjectSync( const QString &projectFullName ); + //! Stops a running sync process if there is one for project specified by projectId + void stopProjectSync( const QString &projectId ) const; - Q_INVOKABLE void migrateProjectToMergin( const QString &projectName ); + Q_INVOKABLE void migrateProjectToMergin( const QString &projectName, const QString &projectId ); //! Returns sync progress of specified project in range <0, 1>. Returns -1 if this project is not being synchronised. - qreal syncProgress( const QString &projectFullName ) const; + qreal syncProgress( const QString &projectId ) const; //! Returns true if specified project is being synchronised, false otherwise. - Q_INVOKABLE bool hasPendingSync( const QString &projectFullName ) const; + Q_INVOKABLE bool hasPendingSync( const QString &projectId ) const; + //! Returns list of UUIDs of pending projects QList pendingProjects() const; signals: // Synchronization signals - void syncStarted( const QString &projectFullName ); - void syncCancelled( const QString &projectFullName ); - void syncProgressChanged( const QString &projectFullName, qreal progress ); - void syncFinished( const QString &projectFullName, bool success, int newVersion, bool reloadNeeded ); - - void syncError( const QString &projectFullName, int errorType, bool willRetry = false, const QString &errorMessage = QLatin1String() ); + void syncStarted( const QString &projectId ); + void syncCancelled( const QString &projectId ); + void syncProgressChanged( const QString &projectId, qreal progress ); + void syncFinished( const QString &projectId, bool success, int newVersion, bool reloadNeeded ); + void syncError( const QString &projectId, int errorType, bool willRetry = false, const QString &errorMessage = QLatin1String() ); public slots: @@ -71,8 +68,9 @@ class SynchronizationManager : public QObject * \brief syncProject Starts synchronization of a project if there are local/server changes to be applied * * \param project Project struct instance - * \param withAut Bears an information whether authorization should be included in sync requests. + * \param auth Bears an information whether authorization should be included in sync requests. * Authorization can be omitted for pull of public projects + * \param strategy The fetching strategy to use */ void syncProject( const LocalProject &project, SyncOptions::Authorization auth = SyncOptions::Authorized, SyncOptions::Strategy strategy = SyncOptions::Singleshot ); @@ -80,17 +78,16 @@ class SynchronizationManager : public QObject void syncProject( const Project &project, SyncOptions::Authorization auth = SyncOptions::Authorized, SyncOptions::Strategy strategy = SyncOptions::Singleshot ); // Handling of synchronization changes from MerginApi - void onProjectSyncCanceled( const QString &projectFullName, bool hasError ); - void onProjectSyncProgressChanged( const QString &projectFullName, qreal progress ); - void onProjectSyncFinished( const QString &projectFullName, bool successfully, int version ); - void onProjectSyncFailure( const QString &message, const QString &topic, int httpCode, const QString &projectFullName ); - void onProjectAttachedToMergin( const QString &projectFullName, const QString &previousName ); - void onProjectReloadNeededAfterSync( const QString &projectFullName ); - void onProjectCreated( const QString &projectName, bool result ); + void onProjectSyncCanceled( const QString &projectId ); + void onProjectSyncProgressChanged( const QString &projectId, qreal progress ); + void onProjectSyncFinished( const QString &projectId, bool successfully, int version ); + void onProjectSyncFailure( const QString &message, int httpCode, const QString &projectId ); + void onProjectReloadNeededAfterSync( const QString &projectId ); + void onProjectCreated( const QString &projectId, bool result ); private: - // Hashmap of currently running synchronizations, key: project full name + // Hashmap of currently running synchronizations, key: project ID QHash mSyncProcesses; MerginApi *mMerginApi = nullptr; // not owned diff --git a/app/test/testmaptools.cpp b/app/test/testmaptools.cpp index faccb1d7a..568276fb7 100644 --- a/app/test/testmaptools.cpp +++ b/app/test/testmaptools.cpp @@ -352,7 +352,7 @@ void TestMapTools::testMeasuring() QVERIFY( measurementTool->recordedGeometry().wkbType() == Qgis::WkbType::Polygon ); - QgsGeometry polygonGeometry = QgsGeometry::fromPolygonXY( QList> () << points ); + QgsGeometry polygonGeometry = QgsGeometry::fromPolygonXY( QList> () << points ); double expectedArea = distanceArea.measureArea( polygonGeometry ); QCOMPARE( measurementTool->area(), expectedArea ); diff --git a/app/test/testmerginapi.cpp b/app/test/testmerginapi.cpp index 8199deffa..062d05df9 100644 --- a/app/test/testmerginapi.cpp +++ b/app/test/testmerginapi.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include "projectchecksumcache.h" @@ -20,18 +20,6 @@ const QString TestMerginApi::TEST_PROJECT_NAME = "TEMPORARY_TEST_PROJECT"; const QString TestMerginApi::TEST_EMPTY_FILE_NAME = "test_empty_file.md"; - -static MerginProject _findProjectByName( const QString &projectNamespace, const QString &projectName, const MerginProjectsList &projects ) -{ - for ( MerginProject project : projects ) - { - if ( project.projectName == projectName && project.projectNamespace == projectNamespace ) - return project; - } - return MerginProject(); -} - - TestMerginApi::TestMerginApi( MerginApi *api ) { mApi = api; @@ -39,13 +27,13 @@ TestMerginApi::TestMerginApi( MerginApi *api ) mSyncManager = std::make_unique( mApi ); - mLocalProjectsModel = std::unique_ptr( new ProjectsModel ); + mLocalProjectsModel = std::make_unique(); mLocalProjectsModel->setModelType( ProjectsModel::LocalProjectsModel ); mLocalProjectsModel->setMerginApi( mApi ); mLocalProjectsModel->setLocalProjectsManager( &mApi->localProjectsManager() ); mLocalProjectsModel->setSyncManager( mSyncManager.get() ); - mWorkspaceProjectsModel = std::unique_ptr( new ProjectsModel ); + mWorkspaceProjectsModel = std::make_unique(); mWorkspaceProjectsModel->setModelType( ProjectsModel::WorkspaceProjectsModel ); mWorkspaceProjectsModel->setMerginApi( mApi ); mWorkspaceProjectsModel->setLocalProjectsManager( &mApi->localProjectsManager() ); @@ -56,6 +44,9 @@ TestMerginApi::~TestMerginApi() = default; void TestMerginApi::initTestCase() { + // ping API was late and tests were failing because they didn't delete projects on start + QSignalSpy spy( mApi, &MerginApi::pingMerginFinished ); + spy.wait( TestUtils::LONG_REPLY ); QString apiRoot, username, password, workspace; TestUtils::merginGetAuthCredentials( mApi, apiRoot, username, password ); @@ -75,7 +66,7 @@ void TestMerginApi::initTestCase() QVERIFY( !mWorkspaceName.isEmpty() ); - QDir testDataDir( TEST_DATA_DIR ); + const QDir testDataDir( TEST_DATA_DIR ); mTestDataPath = testDataDir.canonicalPath(); // get rid of any ".." that may cause problems later qDebug() << "test data dir:" << mTestDataPath; @@ -83,14 +74,22 @@ void TestMerginApi::initTestCase() QDir testProjectsExtraDir( testDataDir.path() + "/../temp_extra_projects" ); if ( testProjectsExtraDir.exists() ) testProjectsExtraDir.removeRecursively(); - QDir( testDataDir.path() + "/.." ).mkpath( "temp_extra_projects" ); - QString projectsExtraDir = testProjectsExtraDir.canonicalPath() + "/"; + if ( !QDir( testDataDir.path() + "/.." ).mkpath( "temp_extra_projects" ) ) + { + qDebug() << QString( "Failed to create directory for temp_extra_projects" ); + }; + const QString projectsExtraDir = testProjectsExtraDir.canonicalPath() + "/"; // create extra API to do requests we are not testing (as if some other user did those) mLocalProjectsExtra = new LocalProjectsManager( projectsExtraDir ); mApiExtra = new MerginApi( *mLocalProjectsExtra ); mApiExtra->setApiRoot( mApi->apiRoot() ); + + // ping API was late and tests were failing because they didn't delete projects on start + QSignalSpy spy2( mApiExtra, &MerginApi::pingMerginFinished ); + spy2.wait( TestUtils::LONG_REPLY ); + if ( TestUtils::needsToAuthorizeAgain( mApiExtra, username ) ) { TestUtils::authorizeUser( mApiExtra, username, password ); @@ -114,15 +113,16 @@ void TestMerginApi::cleanupTestCase() void TestMerginApi::testListProject() { - QString projectName = "testListProject"; + const QString projectName = "testListProject"; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); deleteRemoteProjectNow( mApi, mWorkspaceName, projectName ); // check that there's no testListProject MerginProjectsList projects = getProjectList(); - QVERIFY( !_findProjectByName( mWorkspaceName, projectName, projects ).isValid() ); - QVERIFY( !mApi->localProjectsManager().projectFromMerginName( mWorkspaceName, projectName ).isValid() ); + QVERIFY( !TestUtils::findProjectByName( projectFullName, projects ).isValid() ); + QVERIFY( !TestUtils::findProjectByName( projectFullName, mApi->localProjectsManager().projects().values() ).isValid() ); // create the project on the server (the content is not important) createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/", false ); @@ -130,28 +130,27 @@ void TestMerginApi::testListProject() // check the project exists on the server projects = getProjectList(); - QVERIFY( _findProjectByName( mWorkspaceName, projectName, projects ).isValid() ); + QVERIFY( TestUtils::findProjectByName( projectFullName, projects ).isValid() ); // project is not available locally, so it has no entry - QVERIFY( !mApi->localProjectsManager().projectFromMerginName( mWorkspaceName, projectName ).isValid() ); + QVERIFY( !TestUtils::findProjectByName( projectFullName, mApi->localProjectsManager().projects().values() ).isValid() ); } void TestMerginApi::testListProjectsByName() { - QString projectName = "testListProjectByName"; + const QString projectName = "testListProjectByName"; // create the project on the server with other client createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); - QByteArray oldToken = mApi->userAuth()->authToken(); + const QByteArray oldToken = mApi->userAuth()->authToken(); // let's invalidate main client's auth token and see if the listProjectsByName gets new one // set token's expiration to 3 secs ago - QDateTime now = QDateTime::currentDateTimeUtc().addSecs( -3 ); + const QDateTime now = QDateTime::currentDateTimeUtc().addSecs( -3 ); mApi->userAuth()->setTokenExpiration( now ); - QStringList projects; - projects.append( MerginApi::getFullProjectName( mWorkspaceName, projectName ) ); + const QStringList projects{ CoreUtils::getFullProjectName( mWorkspaceName, projectName ) }; QSignalSpy responseReceived( mApi, &MerginApi::listProjectsByNameFinished ); mApi->listProjectsByName( projects ); @@ -159,29 +158,28 @@ void TestMerginApi::testListProjectsByName() QVERIFY( oldToken != mApi->userAuth()->authToken() ); - MerginProjectsList receivedProjects = projectListFromSpy( responseReceived ); + const MerginProjectsList receivedProjects = projectListFromSpy( responseReceived ); QVERIFY( receivedProjects.count() == 1 ); - MerginProject ourProject = receivedProjects.at( 0 ); + const MerginProject &ourProject = receivedProjects.at( 0 ); QVERIFY( ourProject.remoteError.isEmpty() ); } -/** - * Download project from a scratch using fetch endpoint. - */ void TestMerginApi::testDownloadProject() { // create the project on the server (the content is not important) - QString projectName = "testDownloadProject"; - QString projectNamespace = mWorkspaceName; + const QString projectName = "testDownloadProject"; + const QString projectNamespace = mWorkspaceName; + const QString projectFullName = CoreUtils::getFullProjectName( projectNamespace, projectName ); createRemoteProject( mApiExtra, projectNamespace, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); // add an entry about this project to main API - otherwise it fails QCOMPARE( mApi->transactions().count(), 0 ); + const QString projectId = projectIdFromProjectFullName( mApi, projectNamespace, projectName ); // try to download the project QSignalSpy spy( mApi, &MerginApi::syncProjectFinished ); - mApi->pullProject( projectNamespace, projectName ); + mApi->pullProject( projectFullName, projectId ); QCOMPARE( mApi->transactions().count(), 1 ); QVERIFY( spy.wait( TestUtils::LONG_REPLY * 5 ) ); QCOMPARE( spy.count(), 1 ); @@ -189,12 +187,12 @@ void TestMerginApi::testDownloadProject() QCOMPARE( mApi->transactions().count(), 0 ); // check that the local projects are updated - QVERIFY( mApi->localProjectsManager().projectFromMerginName( mWorkspaceName, projectName ).isValid() ); + QVERIFY( mApi->localProjectsManager().projectFromProjectId( projectId ).isValid() ); - // update model to have latest info + // update model to have the latest info refreshProjectsModel( ProjectsModel::LocalProjectsModel ); - Project project = mLocalProjectsModel->projectFromId( MerginApi::getFullProjectName( projectNamespace, projectName ) ); + Project project = mLocalProjectsModel->projectFromId( projectId ); QVERIFY( project.isLocal() && project.isMergin() ); QCOMPARE( project.local.projectDir, mApi->projectsPath() + "/" + projectName ); @@ -202,7 +200,7 @@ void TestMerginApi::testDownloadProject() QCOMPARE( project.mergin.serverVersion, 1 ); QCOMPARE( project.mergin.status, ProjectStatus::UpToDate ); - bool downloadSuccessful = mApi->localProjectsManager().projectFromMerginName( projectNamespace, projectName ).isValid(); + bool downloadSuccessful = mApi->localProjectsManager().projectFromProjectId( projectId ).isValid(); QVERIFY( downloadSuccessful ); // there should be something in the directory @@ -218,43 +216,45 @@ void TestMerginApi::testDownloadProjectSpecChars() // Create and upload project with project file containing special chars in its name. // Especially testing a name containing "+" sign which was converted into a space when a download query gets to Mergin server // https://doc.qt.io/qt-5/qurlquery.html#handling-of-spaces-and-plus - QString projectName = "testDownloadProjectSpecChars"; - QString projectNamespace = mWorkspaceName; - QString projectDir = mApi->projectsPath() + "/" + projectName + "/"; + const QString projectName = "testDownloadProjectSpecChars"; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); + const QString projectDir = mApi->projectsPath() + "/" + projectName + "/"; + // TODO: maybe use createRemoteProject instead of this // First remove project on remote server (from previous test runs) - deleteRemoteProjectNow( mApi, projectNamespace, projectName ); + deleteRemoteProjectNow( mApi, mWorkspaceName, projectName ); // create an empty project on the server QSignalSpy spy( mApi, &MerginApi::projectCreated ); - mApi->createProject( projectNamespace, projectName, true ); + mApi->createProject( mWorkspaceName, projectName, CoreUtils::uuidWithoutBraces( QUuid::createUuid() ), true ); QVERIFY( spy.wait( TestUtils::SHORT_REPLY ) ); QCOMPARE( spy.count(), 1 ); QCOMPARE( spy.takeFirst().at( 1 ).toBool(), true ); // make MerginApi aware of the project and its directory - mApi->localProjectsManager().addMerginProject( projectDir, projectNamespace, projectName ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); + mApi->localProjectsManager().addMerginProject( projectDir, mWorkspaceName, projectName, projectId ); // Copy data - QString sourcePath = mTestDataPath + "/" + TestMerginApi::TEST_PROJECT_NAME + "/"; + const QString sourcePath = mTestDataPath + "/" + TEST_PROJECT_NAME + "/"; InputUtils::cpDir( sourcePath, projectDir ); // Add special characters in the project file name QFile projectFile( projectDir + "/project.qgs" ); QVERIFY( projectFile.exists() ); - QString specChars( "_-. " ); // can't start with dot or space - QString newProjectFileName = QString( "%1.qgs" ).arg( specChars ); + const QString specChars( "_-. " ); // can't start with dot or space + const QString newProjectFileName = QString( "%1.qgs" ).arg( specChars ); QVERIFY( projectFile.rename( projectDir + "/" + newProjectFileName ) ); // Upload data QSignalSpy spy2( mApi, &MerginApi::syncProjectFinished ); - mApi->pushProject( projectNamespace, projectName ); + mApi->pushProject( projectFullName, projectId ); QVERIFY( spy2.wait( TestUtils::LONG_REPLY ) ); QCOMPARE( spy2.count(), 1 ); QList arguments = spy2.takeFirst(); QVERIFY( arguments.at( 2 ).toBool() ); // Download project and check if the project file is there - downloadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + downloadRemoteProject( mApiExtra, projectFullName, projectId ); QString projectDirExtra = mApiExtra->projectsPath() + "/" + projectName; QFile projectFileExtra( projectDirExtra + "/" + newProjectFileName ); QVERIFY( projectFileExtra.exists() ); @@ -262,19 +262,21 @@ void TestMerginApi::testDownloadProjectSpecChars() void TestMerginApi::testCancelDownloadProject() { - QString projectName = "testCancelDownloadProject"; + const QString projectName = "testCancelDownloadProject"; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); - createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + TestMerginApi::TEST_PROJECT_NAME + "/" ); + createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); QCOMPARE( mApi->transactions().count(), 0 ); - QString projectDir = mApi->projectsPath() + "/" + projectName + "/"; + const QString projectDir = mApi->projectsPath() + "/" + projectName + "/"; + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); // Test download and cancel before transaction actually starts QSignalSpy spy5( mApi, &MerginApi::syncProjectFinished ); - mApi->pullProject( mWorkspaceName, projectName ); + mApi->pullProject( projectFullName, projectId ); QCOMPARE( mApi->transactions().count(), 1 ); - mApi->cancelPull( MerginApi::getFullProjectName( mWorkspaceName, projectName ) ); + mApi->cancelPull( projectId ); // no need to wait for the signal here - as we call abort() the reply's finished() signal is immediately emitted QCOMPARE( spy5.count(), 1 ); @@ -286,39 +288,39 @@ void TestMerginApi::testCancelDownloadProject() QCOMPARE( mApi->transactions().count(), 0 ); - // Test download and cancel after transcation starts + // Test download and cancel after transaction starts QSignalSpy spy6( mApi, &MerginApi::pullFilesStarted ); - mApi->pullProject( mWorkspaceName, projectName ); + mApi->pullProject( projectFullName, projectId ); QVERIFY( spy6.wait( TestUtils::LONG_REPLY ) ); QCOMPARE( spy6.count(), 1 ); QSignalSpy spy7( mApi, &MerginApi::syncProjectFinished ); - mApi->cancelPull( MerginApi::getFullProjectName( mWorkspaceName, projectName ) ); + mApi->cancelPull( projectId ); // no need to wait for the signal here - as we call abort() the reply's finished() signal is immediately emitted QCOMPARE( spy7.count(), 1 ); arguments = spy7.takeFirst(); QVERIFY( !arguments.at( 1 ).toBool() ); - QFileInfo info( projectDir ); - QDir dir( projectDir ); + const QFileInfo info( projectDir ); + const QDir dir( projectDir ); QCOMPARE( info.size(), 0 ); QVERIFY( dir.isEmpty() ); } void TestMerginApi::testCreateProjectTwice() { - QString projectName = "testCreateProjectTwice"; - QString projectNamespace = mWorkspaceName; + const QString projectName = "testCreateProjectTwice"; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); // First remove project on remote server (from previous test runs) - deleteRemoteProjectNow( mApi, projectNamespace, projectName ); + deleteRemoteProjectNow( mApi, mWorkspaceName, projectName ); MerginProjectsList projects = getProjectList(); - QVERIFY( !_findProjectByName( projectNamespace, projectName, projects ).isValid() ); + QVERIFY( !TestUtils::findProjectByName( projectFullName, projects ).isValid() ); QSignalSpy spy( mApi, &MerginApi::projectCreated ); - mApi->createProject( projectNamespace, projectName, true ); + mApi->createProject( mWorkspaceName, projectName, CoreUtils::uuidWithoutBraces( QUuid::createUuid() ), true ); QVERIFY( spy.wait( TestUtils::SHORT_REPLY ) ); QCOMPARE( spy.count(), 1 ); QCOMPARE( spy.takeFirst().at( 1 ).toBool(), true ); @@ -327,60 +329,54 @@ void TestMerginApi::testCreateProjectTwice() refreshProjectsModel( ProjectsModel::WorkspaceProjectsModel ); QVERIFY( mWorkspaceProjectsModel->rowCount() ); - QVERIFY( _findProjectByName( projectNamespace, projectName, projects ).isValid() ); + QVERIFY( TestUtils::findProjectByName( projectFullName, projects ).isValid() ); // Create again, expecting error QSignalSpy spy2( mApi, &MerginApi::networkErrorOccurred ); - mApi->createProject( projectNamespace, projectName, true ); + mApi->createProject( mWorkspaceName, projectName, CoreUtils::uuidWithoutBraces( QUuid::createUuid() ), true ); QVERIFY( spy2.wait( TestUtils::SHORT_REPLY ) ); QCOMPARE( spy2.count(), 1 ); - QList arguments = spy2.takeFirst(); - QVERIFY( arguments.at( 0 ).type() == QVariant::String ); - QVERIFY( arguments.at( 1 ).type() == QVariant::String ); + const QList arguments = spy2.takeFirst(); + QVERIFY( arguments.at( 0 ).metaType().id() == QMetaType::QString ); + QVERIFY( arguments.at( 1 ).metaType().id() == QMetaType::Int ); - QCOMPARE( arguments.at( 1 ).toString(), QStringLiteral( "Mergin API error: createProject" ) ); + QCOMPARE( arguments.at( 1 ).toInt(), 409 ); //Clean created project - deleteRemoteProjectNow( mApi, projectNamespace, projectName ); + deleteRemoteProjectNow( mApi, mWorkspaceName, projectName ); projects = getProjectList(); - QVERIFY( !_findProjectByName( projectNamespace, projectName, projects ).isValid() ); + QVERIFY( !TestUtils::findProjectByName( projectFullName, projects ).isValid() ); } void TestMerginApi::testDeleteNonExistingProject() { - // Checks if projects doesn't exist - QString projectName = "testDeleteNonExistingProject"; - QString projectNamespace = mWorkspaceName; - MerginProjectsList projects = getProjectList(); - QVERIFY( !_findProjectByName( projectNamespace, projectName, projects ).isValid() ); - // Try to delete non-existing project QSignalSpy spy( mApi, &MerginApi::networkErrorOccurred ); - mApi->deleteProject( projectNamespace, projectName ); + mApi->deleteProject( CoreUtils::uuidWithoutBraces( QUuid() ) ); QVERIFY( spy.wait( TestUtils::SHORT_REPLY ) ); - QList arguments = spy.takeFirst(); - QVERIFY( arguments.at( 0 ).type() == QVariant::String ); - QVERIFY( arguments.at( 1 ).type() == QVariant::String ); - QCOMPARE( arguments.at( 1 ).toString(), QStringLiteral( "Mergin API error: deleteProject" ) ); + const QList arguments = spy.takeFirst(); + QVERIFY( arguments.at( 0 ).metaType().id() == QMetaType::QString ); + QVERIFY( arguments.at( 1 ).metaType().id() == QMetaType::Int ); + QCOMPARE( arguments.at( 1 ).toInt(), 404 ); } void TestMerginApi::testCreateDeleteProject() { // Create a project - QString projectName = "testCreateDeleteProject"; - QString projectNamespace = mWorkspaceName; + const QString projectName = "testCreateDeleteProject"; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); // First remove project on remote server (from previous test runs) - deleteRemoteProjectNow( mApi, projectNamespace, projectName ); + deleteRemoteProjectNow( mApi, mWorkspaceName, projectName ); MerginProjectsList projects = getProjectList(); - QVERIFY( !_findProjectByName( projectNamespace, projectName, projects ).isValid() ); + QVERIFY( !TestUtils::findProjectByName( projectFullName, projects ).isValid() ); QSignalSpy spy( mApi, &MerginApi::projectCreated ); - mApi->createProject( projectNamespace, projectName, true ); + mApi->createProject( mWorkspaceName, projectName, CoreUtils::uuidWithoutBraces( QUuid::createUuid() ), true ); QVERIFY( spy.wait( TestUtils::SHORT_REPLY ) ); QCOMPARE( spy.count(), 1 ); QCOMPARE( spy.takeFirst().at( 1 ).toBool(), true ); @@ -389,39 +385,40 @@ void TestMerginApi::testCreateDeleteProject() refreshProjectsModel( ProjectsModel::WorkspaceProjectsModel ); QVERIFY( mWorkspaceProjectsModel->rowCount() ); - Q_ASSERT( _findProjectByName( projectNamespace, projectName, projects ).isValid() ); + Q_ASSERT( TestUtils::findProjectByName( projectFullName, projects ).isValid() ); // Delete created project - deleteRemoteProjectNow( mApi, projectNamespace, projectName ); + deleteRemoteProjectNow( mApi, mWorkspaceName, projectName ); projects = getProjectList(); - QVERIFY( !_findProjectByName( projectNamespace, projectName, projects ).isValid() ); + QVERIFY( !TestUtils::findProjectByName( projectFullName, projects ).isValid() ); } void TestMerginApi::testUploadProject() { - QString projectName = "testUploadProject"; - QString projectNamespace = mWorkspaceName; + const QString projectName = "testUploadProject"; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); QString projectDir = mApi->projectsPath() + "/" + projectName; // clean leftovers from previous run first - deleteRemoteProjectNow( mApi, projectNamespace, projectName ); + deleteRemoteProjectNow( mApi, mWorkspaceName, projectName ); QSignalSpy spy0( mApiExtra, &MerginApi::projectCreated ); - mApiExtra->createProject( projectNamespace, projectName, true ); + mApiExtra->createProject( mWorkspaceName, projectName, CoreUtils::uuidWithoutBraces( QUuid::createUuid() ), true ); QVERIFY( spy0.wait( TestUtils::LONG_REPLY ) ); QCOMPARE( spy0.count(), 1 ); QCOMPARE( spy0.takeFirst().at( 1 ).toBool(), true ); MerginProjectsList projects = getProjectList(); - QVERIFY( _findProjectByName( projectNamespace, projectName, projects ).isValid() ); + QVERIFY( TestUtils::findProjectByName( projectFullName, projects ).isValid() ); // copy project's test data to the new project directory QVERIFY( InputUtils::cpDir( mTestDataPath + "/" + TEST_PROJECT_NAME, projectDir ) ); - mApi->localProjectsManager().addMerginProject( projectDir, projectNamespace, projectName ); + const QString projectId = TestUtils::findProjectByName( projectFullName, projects ).id(); + mApi->localProjectsManager().addMerginProject( projectDir, mWorkspaceName, projectName, projectId ); // project info does not have any version information yet - Project project0 = mLocalProjectsModel->projectFromId( MerginApi::getFullProjectName( projectNamespace, projectName ) ); + Project project0 = mLocalProjectsModel->projectFromId( projectId ); QVERIFY( project0.isLocal() && !project0.isMergin() ); QCOMPARE( project0.local.localVersion, -1 ); @@ -431,8 +428,8 @@ void TestMerginApi::testUploadProject() // QSignalSpy spy( mApi, &MerginApi::syncProjectFinished ); - mApi->pushProject( projectNamespace, projectName ); - mApi->cancelPush( MerginApi::getFullProjectName( projectNamespace, projectName ) ); + mApi->pushProject( projectFullName, projectId ); + mApi->cancelPush( projectId ); // no need to wait for the signal here - as we call abort() the reply's finished() signal is immediately emitted QCOMPARE( spy.count(), 1 ); @@ -440,7 +437,7 @@ void TestMerginApi::testUploadProject() QVERIFY( !arguments.at( 1 ).toBool() ); // server version is still not available (cancelled before project info) - Project project1 = mLocalProjectsModel->projectFromId( MerginApi::getFullProjectName( projectNamespace, projectName ) ); + Project project1 = mLocalProjectsModel->projectFromId( projectId ); QVERIFY( project1.isLocal() && !project1.isMergin() ); QCOMPARE( project1.local.localVersion, -1 ); @@ -451,12 +448,12 @@ void TestMerginApi::testUploadProject() QSignalSpy spyX( mApi, &MerginApi::syncProjectFinished ); QSignalSpy spyY( mApi, &MerginApi::pushFilesStarted ); - mApi->pushProject( projectNamespace, projectName ); + mApi->pushProject( projectFullName, projectId ); QVERIFY( spyY.wait( TestUtils::LONG_REPLY ) ); QCOMPARE( spyY.count(), 1 ); QSignalSpy spyCancel( mApi, &MerginApi::pushCanceled ); - mApi->cancelPush( MerginApi::getFullProjectName( projectNamespace, projectName ) ); + mApi->cancelPush( projectId ); QVERIFY( spyCancel.wait( TestUtils::LONG_REPLY ) ); QCOMPARE( spyCancel.count(), 1 ); @@ -466,13 +463,13 @@ void TestMerginApi::testUploadProject() QVERIFY( !argumentsX.at( 1 ).toBool() ); // server version is now available (cancelled after project info), but after projects model refresh - Project project2 = mLocalProjectsModel->projectFromId( MerginApi::getFullProjectName( projectNamespace, projectName ) ); + Project project2 = mLocalProjectsModel->projectFromId( projectId ); QVERIFY( project2.isLocal() && !project2.isMergin() ); QCOMPARE( project2.local.localVersion, -1 ); refreshProjectsModel( ProjectsModel::LocalProjectsModel ); - project2 = mLocalProjectsModel->projectFromId( MerginApi::getFullProjectName( projectNamespace, projectName ) ); + project2 = mLocalProjectsModel->projectFromId( projectId ); QVERIFY( project2.isLocal() && project2.isMergin() ); QCOMPARE( project2.local.localVersion, -1 ); QCOMPARE( project2.mergin.serverVersion, 0 ); @@ -481,13 +478,13 @@ void TestMerginApi::testUploadProject() // try to upload - and let the upload finish successfully // - mApi->pushProject( projectNamespace, projectName ); + mApi->pushProject( projectFullName, projectId ); QSignalSpy spy2( mApi, &MerginApi::syncProjectFinished ); QVERIFY( spy2.wait( TestUtils::LONG_REPLY ) ); QCOMPARE( spy2.count(), 1 ); - Project project3 = mLocalProjectsModel->projectFromId( MerginApi::getFullProjectName( projectNamespace, projectName ) ); + Project project3 = mLocalProjectsModel->projectFromId( projectId ); QVERIFY( project3.isLocal() && project3.isMergin() ); QCOMPARE( project3.local.localVersion, 1 ); QCOMPARE( project3.mergin.serverVersion, 1 ); @@ -496,90 +493,94 @@ void TestMerginApi::testUploadProject() void TestMerginApi::testMultiChunkUploadDownload() { - // this will try to upload a file that needs to be split into multiple chunks - // and then also download it correctly again in a clean new download - - QString projectName = "testMultiChunkUploadDownload"; + const QString projectName = "testMultiChunkUploadDownload"; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); + + downloadRemoteProject( mApi, projectFullName, projectId ); // create a big file (21mb) - QString bigFilePath = mApi->projectsPath() + "/" + projectName + "/" + "big_file.dat"; + const QString bigFilePath = mApi->projectsPath() + "/" + projectName + "/" + "big_file.dat"; QFile bigFile( bigFilePath ); QVERIFY( bigFile.open( QIODevice::WriteOnly ) ); for ( int i = 0; i < 21; ++i ) // 21 times 1mb -> should be three chunks when chunk size == 10mb bigFile.write( QByteArray( 1024 * 1024, static_cast( 'A' + i ) ) ); // AAAA.....BBBB.....CCCC..... bigFile.close(); - QByteArray checksum = CoreUtils::calculateChecksum( bigFilePath ); + const QByteArray checksum = CoreUtils::calculateChecksum( bigFilePath ); QVERIFY( !checksum.isEmpty() ); // upload - uploadRemoteProject( mApi, mWorkspaceName, projectName ); + uploadRemoteProject( mApi, projectFullName, projectId ); // download again - deleteLocalProject( mApi, mWorkspaceName, projectName ); + deleteLocalProject( mApi, projectId ); QVERIFY( !QFileInfo::exists( bigFilePath ) ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); // verify it's there and with correct content - QByteArray checksum2 = CoreUtils::calculateChecksum( bigFilePath ); + const QByteArray checksum2 = CoreUtils::calculateChecksum( bigFilePath ); QVERIFY( QFileInfo::exists( bigFilePath ) ); QCOMPARE( checksum, checksum2 ); } void TestMerginApi::testEmptyFileUploadDownload() { - // test will try to upload a project with empty file - - QString projectName = QStringLiteral( "testEmptyFileUploadDownload" ); + const QString projectName = QStringLiteral( "testEmptyFileUploadDownload" ); + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); + + downloadRemoteProject( mApi, projectFullName, projectId ); - QString emptyFileDestinationPath = mApi->projectsPath() + "/" + projectName + "/" + TEST_EMPTY_FILE_NAME; + const QString emptyFileDestinationPath = mApi->projectsPath() + "/" + projectName + "/" + TEST_EMPTY_FILE_NAME; // copy empty file to project QFile::copy( mTestDataPath + "/" + TEST_EMPTY_FILE_NAME, emptyFileDestinationPath ); QVERIFY( QFileInfo::exists( emptyFileDestinationPath ) ); - QByteArray checksum = CoreUtils::calculateChecksum( emptyFileDestinationPath ); + const QByteArray checksum = CoreUtils::calculateChecksum( emptyFileDestinationPath ); QVERIFY( !checksum.isEmpty() ); //upload - uploadRemoteProject( mApi, mWorkspaceName, projectName ); + uploadRemoteProject( mApi, projectFullName, projectId ); //download again - deleteLocalProject( mApi, mWorkspaceName, projectName ); + deleteLocalProject( mApi, projectId ); QVERIFY( !QFileInfo::exists( emptyFileDestinationPath ) ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); // verify it's there and with correct content - QByteArray checksum2 = CoreUtils::calculateChecksum( emptyFileDestinationPath ); + const QByteArray checksum2 = CoreUtils::calculateChecksum( emptyFileDestinationPath ); QVERIFY( QFileInfo::exists( emptyFileDestinationPath ) ); QCOMPARE( checksum, checksum2 ); } void TestMerginApi::testPushAddedFile() { - QString projectName = "testPushAddedFile"; + const QString projectName = "testPushAddedFile"; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); refreshProjectsModel( ProjectsModel::WorkspaceProjectsModel ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); + + downloadRemoteProject( mApi, projectFullName, projectId ); - Project project0 = mWorkspaceProjectsModel->projectFromId( MerginApi::getFullProjectName( mWorkspaceName, projectName ) ); + Project project0 = mWorkspaceProjectsModel->projectFromId( projectId ); QVERIFY( project0.isLocal() && project0.isMergin() ); QCOMPARE( project0.local.localVersion, 1 ); QCOMPARE( project0.mergin.serverVersion, 1 ); QCOMPARE( project0.mergin.status, ProjectStatus::UpToDate ); // add a single file - QString newFilePath = mApi->projectsPath() + "/" + projectName + "/added.txt"; + const QString newFilePath = mApi->projectsPath() + "/" + projectName + "/added.txt"; QFile file( newFilePath ); QVERIFY( file.open( QIODevice::WriteOnly ) ); file.write( "added file content\n" ); @@ -588,26 +589,26 @@ void TestMerginApi::testPushAddedFile() // check that the status is "modified" refreshProjectsModel( ProjectsModel::WorkspaceProjectsModel ); // force update of status - Project project1 = mWorkspaceProjectsModel->projectFromId( MerginApi::getFullProjectName( mWorkspaceName, projectName ) ); + Project project1 = mWorkspaceProjectsModel->projectFromId( projectId ); QVERIFY( project1.isLocal() && project1.isMergin() ); QCOMPARE( project1.local.localVersion, 1 ); QCOMPARE( project1.mergin.serverVersion, 1 ); QCOMPARE( project1.mergin.status, ProjectStatus::NeedsSync ); // upload - uploadRemoteProject( mApi, mWorkspaceName, projectName ); + uploadRemoteProject( mApi, projectFullName, projectId ); - Project project2 = mWorkspaceProjectsModel->projectFromId( MerginApi::getFullProjectName( mWorkspaceName, projectName ) ); + Project project2 = mWorkspaceProjectsModel->projectFromId( projectId ); QVERIFY( project2.isLocal() && project2.isMergin() ); QCOMPARE( project2.local.localVersion, 2 ); QCOMPARE( project2.mergin.serverVersion, 2 ); QCOMPARE( project2.mergin.status, ProjectStatus::UpToDate ); - deleteLocalProject( mApi, mWorkspaceName, projectName ); + deleteLocalProject( mApi, projectId ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); - Project project3 = mWorkspaceProjectsModel->projectFromId( MerginApi::getFullProjectName( mWorkspaceName, projectName ) ); + Project project3 = mWorkspaceProjectsModel->projectFromId( projectId ); QVERIFY( project3.isLocal() && project3.isMergin() ); QCOMPARE( project3.local.localVersion, 2 ); QCOMPARE( project3.mergin.serverVersion, 2 ); @@ -620,17 +621,17 @@ void TestMerginApi::testPushAddedFile() void TestMerginApi::testPushRemovedFile() { - // download a project, then remove a file locally and upload the project. - // we then check that the file is really removed on the subsequent download. - - QString projectName = "testPushRemovedFile"; + const QString projectName = "testPushRemovedFile"; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); refreshProjectsModel( ProjectsModel::WorkspaceProjectsModel ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); + + downloadRemoteProject( mApi, projectFullName, projectId ); - Project project0 = mWorkspaceProjectsModel->projectFromId( MerginApi::getFullProjectName( mWorkspaceName, projectName ) ); + Project project0 = mWorkspaceProjectsModel->projectFromId( projectId ); QVERIFY( project0.isLocal() && project0.isMergin() ); QCOMPARE( project0.local.localVersion, 1 ); QCOMPARE( project0.mergin.serverVersion, 1 ); @@ -646,7 +647,7 @@ void TestMerginApi::testPushRemovedFile() // check that it is considered as modified now refreshProjectsModel( ProjectsModel::WorkspaceProjectsModel ); // force update of status - Project project1 = mWorkspaceProjectsModel->projectFromId( MerginApi::getFullProjectName( mWorkspaceName, projectName ) ); + Project project1 = mWorkspaceProjectsModel->projectFromId( projectId ); QVERIFY( project1.isLocal() && project1.isMergin() ); QCOMPARE( project1.local.localVersion, 1 ); QCOMPARE( project1.mergin.serverVersion, 1 ); @@ -654,19 +655,19 @@ void TestMerginApi::testPushRemovedFile() // upload changes - uploadRemoteProject( mApi, mWorkspaceName, projectName ); + uploadRemoteProject( mApi, projectFullName, projectId ); - Project project2 = mWorkspaceProjectsModel->projectFromId( MerginApi::getFullProjectName( mWorkspaceName, projectName ) ); + Project project2 = mWorkspaceProjectsModel->projectFromId( projectId ); QVERIFY( project2.isLocal() && project2.isMergin() ); QCOMPARE( project2.local.localVersion, 2 ); QCOMPARE( project2.mergin.serverVersion, 2 ); QCOMPARE( project2.mergin.status, ProjectStatus::UpToDate ); - deleteLocalProject( mApi, mWorkspaceName, projectName ); + deleteLocalProject( mApi, projectId ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); - Project project3 = mWorkspaceProjectsModel->projectFromId( MerginApi::getFullProjectName( mWorkspaceName, projectName ) ); + Project project3 = mWorkspaceProjectsModel->projectFromId( projectId ); QVERIFY( project3.isLocal() && project3.isMergin() ); QCOMPARE( project3.local.localVersion, 2 ); QCOMPARE( project3.mergin.serverVersion, 2 ); @@ -682,19 +683,22 @@ void TestMerginApi::testPushRemovedFile() void TestMerginApi::testPushModifiedFile() { - QString projectName = "testPushModifiedFile"; + const QString projectName = "testPushModifiedFile"; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); refreshProjectsModel( ProjectsModel::WorkspaceProjectsModel ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); + + downloadRemoteProject( mApi, projectFullName, projectId ); // need to sleep at least for a second so that last modified time // has later timestamp than the last sync (seems there's one second resolution) QTest::qSleep( 1000 ); // modify a single file - QString filename = mApi->projectsPath() + "/" + projectName + "/project.qgs"; + const QString filename = mApi->projectsPath() + "/" + projectName + "/project.qgs"; QFile file( filename ); QVERIFY( file.open( QIODevice::WriteOnly ) ); file.write( QByteArray( "v2" ) ); @@ -702,16 +706,16 @@ void TestMerginApi::testPushModifiedFile() // check that the status is "modified" refreshProjectsModel( ProjectsModel::WorkspaceProjectsModel ); // force update of status - Project project1 = mWorkspaceProjectsModel->projectFromId( MerginApi::getFullProjectName( mWorkspaceName, projectName ) ); + Project project1 = mWorkspaceProjectsModel->projectFromId( projectId ); QVERIFY( project1.isLocal() && project1.isMergin() ); QCOMPARE( project1.local.localVersion, 1 ); QCOMPARE( project1.mergin.serverVersion, 1 ); QCOMPARE( project1.mergin.status, ProjectStatus::NeedsSync ); // upload - uploadRemoteProject( mApi, mWorkspaceName, projectName ); + uploadRemoteProject( mApi, projectFullName, projectId ); - Project project2 = mWorkspaceProjectsModel->projectFromId( MerginApi::getFullProjectName( mWorkspaceName, projectName ) ); + Project project2 = mWorkspaceProjectsModel->projectFromId( projectId ); QVERIFY( project2.isLocal() && project2.isMergin() ); QCOMPARE( project2.local.localVersion, 2 ); QCOMPARE( project2.mergin.serverVersion, 2 ); @@ -719,13 +723,13 @@ void TestMerginApi::testPushModifiedFile() // verify the remote project has updated file - deleteLocalProject( mApi, mWorkspaceName, projectName ); + deleteLocalProject( mApi, projectId ); QVERIFY( !file.open( QIODevice::ReadOnly ) ); // it should not exist at this point - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); - Project project3 = mWorkspaceProjectsModel->projectFromId( MerginApi::getFullProjectName( mWorkspaceName, projectName ) ); + Project project3 = mWorkspaceProjectsModel->projectFromId( projectId ); QVERIFY( project3.isLocal() && project3.isMergin() ); QCOMPARE( project3.local.localVersion, 2 ); QCOMPARE( project3.mergin.serverVersion, 2 ); @@ -738,26 +742,29 @@ void TestMerginApi::testPushModifiedFile() void TestMerginApi::testPushNoChanges() { - QString projectName = "testPushNoChanges"; - QString projectDir = mApi->projectsPath() + "/" + projectName; + const QString projectName = "testPushNoChanges"; + const QString projectDir = mApi->projectsPath() + "/" + projectName; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); refreshProjectsModel( ProjectsModel::WorkspaceProjectsModel ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); + + downloadRemoteProject( mApi, projectFullName, projectId ); // check that the status is still "up-to-date" - Project project1 = mWorkspaceProjectsModel->projectFromId( MerginApi::getFullProjectName( mWorkspaceName, projectName ) ); + Project project1 = mWorkspaceProjectsModel->projectFromId( projectId ); QVERIFY( project1.isLocal() && project1.isMergin() ); QCOMPARE( project1.local.localVersion, 1 ); QCOMPARE( project1.mergin.serverVersion, 1 ); QCOMPARE( project1.mergin.status, ProjectStatus::UpToDate ); // upload - should do nothing - uploadRemoteProject( mApi, mWorkspaceName, projectName ); + uploadRemoteProject( mApi, projectFullName, projectId ); - Project project2 = mWorkspaceProjectsModel->projectFromId( MerginApi::getFullProjectName( mWorkspaceName, projectName ) ); + Project project2 = mWorkspaceProjectsModel->projectFromId( projectId ); QVERIFY( project2.isLocal() && project2.isMergin() ); QCOMPARE( project2.local.localVersion, 1 ); QCOMPARE( project2.mergin.serverVersion, 1 ); @@ -769,45 +776,45 @@ void TestMerginApi::testPushNoChanges() void TestMerginApi::testUpdateAddedFile() { - // this test downloads a project, then a file gets added on the server - // and we check whether the update was correct (i.e. the file got added too) - - QString projectName = "testUpdateAddedFile"; - QString projectDir = mApi->projectsPath() + "/" + projectName; - QString extraProjectDir = mApiExtra->projectsPath() + "/" + projectName; + const QString projectName = "testUpdateAddedFile"; + const QString projectDir = mApi->projectsPath() + "/" + projectName; + const QString extraProjectDir = mApiExtra->projectsPath() + "/" + projectName; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); refreshProjectsModel( ProjectsModel::WorkspaceProjectsModel ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); + // download initial version - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); QVERIFY( !QFile::exists( projectDir + "/test-remote-new.txt" ) ); - Project project0 = mWorkspaceProjectsModel->projectFromId( MerginApi::getFullProjectName( mWorkspaceName, projectName ) ); + Project project0 = mWorkspaceProjectsModel->projectFromId( projectId ); QVERIFY( project0.isLocal() && project0.isMergin() ); QCOMPARE( project0.local.localVersion, 1 ); QCOMPARE( project0.mergin.serverVersion, 1 ); QCOMPARE( project0.mergin.status, ProjectStatus::UpToDate ); - // remove a file on the server - downloadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + // update a file on the server + downloadRemoteProject( mApiExtra, projectFullName, projectId ); writeFileContent( extraProjectDir + "/test-remote-new.txt", QByteArray( "my new content" ) ); - uploadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + uploadRemoteProject( mApiExtra, projectFullName, projectId ); QVERIFY( QFile::exists( extraProjectDir + "/test-remote-new.txt" ) ); // list projects - just so that we can figure out we are behind refreshProjectsModel( ProjectsModel::WorkspaceProjectsModel ); - Project project1 = mWorkspaceProjectsModel->projectFromId( MerginApi::getFullProjectName( mWorkspaceName, projectName ) ); + Project project1 = mWorkspaceProjectsModel->projectFromId( projectId ); QVERIFY( project1.isLocal() && project1.isMergin() ); QCOMPARE( project1.local.localVersion, 1 ); QCOMPARE( project1.mergin.serverVersion, 2 ); QCOMPARE( project1.mergin.status, ProjectStatus::NeedsSync ); // now try to update - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); - Project project2 = mWorkspaceProjectsModel->projectFromId( MerginApi::getFullProjectName( mWorkspaceName, projectName ) ); + Project project2 = mWorkspaceProjectsModel->projectFromId( projectId ); QVERIFY( project2.isLocal() && project2.isMergin() ); QCOMPARE( project2.local.localVersion, 2 ); QCOMPARE( project2.mergin.serverVersion, 2 ); @@ -822,26 +829,26 @@ void TestMerginApi::testUpdateAddedFile() void TestMerginApi::testUpdateRemovedFiles() { - // this tests downloads a project, then a file gets removed on the server - // and we check whether the update was correct (i.e. the file got removed too) - - QString projectName = "testUpdateRemovedFiles"; - QString projectDir = mApi->projectsPath() + "/" + projectName; - QString extraProjectDir = mApiExtra->projectsPath() + "/" + projectName; + const QString projectName = "testUpdateRemovedFiles"; + const QString projectDir = mApi->projectsPath() + "/" + projectName; + const QString extraProjectDir = mApiExtra->projectsPath() + "/" + projectName; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); + // download initial version - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); QVERIFY( QFile::exists( projectDir + "/test1.txt" ) ); // remove a file on the server - downloadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + downloadRemoteProject( mApiExtra, projectFullName, projectId ); QVERIFY( QFile::remove( extraProjectDir + "/test1.txt" ) ); - uploadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + uploadRemoteProject( mApiExtra, projectFullName, projectId ); // now try to update - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); // check that the removed file is not there anymore QVERIFY( QFile::exists( projectDir + "/project.qgs" ) ); @@ -850,24 +857,23 @@ void TestMerginApi::testUpdateRemovedFiles() void TestMerginApi::testUpdateRemovedVsModifiedFiles() { - // this test downloads a project, then a files gets removed on the server, - // but it also gets re-created locally with different content. It should be - // correctly detected that the file is a local modification and it should be kept - - QString projectName = "testUpdateRemovedVsModifiedFiles"; - QString projectDir = mApi->projectsPath() + "/" + projectName; - QString extraProjectDir = mApiExtra->projectsPath() + "/" + projectName; + const QString projectName = "testUpdateRemovedVsModifiedFiles"; + const QString projectDir = mApi->projectsPath() + "/" + projectName; + const QString extraProjectDir = mApiExtra->projectsPath() + "/" + projectName; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); + // download initial version - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); QVERIFY( QFile::exists( projectDir + "/test1.txt" ) ); // remove a file on the server - downloadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + downloadRemoteProject( mApiExtra, projectFullName, projectId ); QVERIFY( QFile::remove( extraProjectDir + "/test1.txt" ) ); - uploadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + uploadRemoteProject( mApiExtra, projectFullName, projectId ); // modify the same file locally QFile file( projectDir + "/test1.txt" ); @@ -876,7 +882,7 @@ void TestMerginApi::testUpdateRemovedVsModifiedFiles() file.close(); // now try to update - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); // check that the file removed on the server is still there, with modified content QVERIFY( QFile::exists( projectDir + "/project.qgs" ) ); @@ -888,26 +894,24 @@ void TestMerginApi::testUpdateRemovedVsModifiedFiles() void TestMerginApi::testConflictRemoteUpdateLocalUpdate() { - // this test downloads a project, makes a local update of a file, - // in the meanwhile it does remote update of the same file to create - // a conflict. Finally it tries to upload the local change to test - // the code responsible for conflict resolution (renames the local file) - - QString projectName = "testConflictRemoteUpdateLocalUpdate"; - QString projectDir = mApi->projectsPath() + "/" + projectName; - QString extraProjectDir = mApiExtra->projectsPath() + "/" + projectName; - QString filename = projectDir + "/test1.txt"; - QString extraFilename = extraProjectDir + "/test1.txt"; + const QString projectName = "testConflictRemoteUpdateLocalUpdate"; + const QString projectDir = mApi->projectsPath() + "/" + projectName; + const QString extraProjectDir = mApiExtra->projectsPath() + "/" + projectName; + const QString filename = projectDir + "/test1.txt"; + const QString extraFilename = extraProjectDir + "/test1.txt"; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); + qDebug() << "download initial version"; - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); qDebug() << "modify test1.txt on the server"; - downloadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + downloadRemoteProject( mApiExtra, projectFullName, projectId ); writeFileContent( extraFilename, QByteArray( "remote content" ) ); - uploadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + uploadRemoteProject( mApiExtra, projectFullName, projectId ); qDebug() << "modify test1.txt locally and do the sync"; writeFileContent( filename, QByteArray( "local content" ) ); @@ -917,20 +921,20 @@ void TestMerginApi::testConflictRemoteUpdateLocalUpdate() // out... in upload's project info handler if there is a need for update, // the upload should be cancelled (or paused to update first). // - downloadRemoteProject( mApi, mWorkspaceName, projectName ); - uploadRemoteProject( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); + uploadRemoteProject( mApi, projectFullName, projectId ); // verify the result: the server version should be in test1.txt // and the local version should go to "test1 (conflicted copy, v).txt" - QString conflictFilename = projectDir + "/test1 (conflicted copy, " + mUsername + " v1).txt"; + const QString conflictFilename = projectDir + "/test1 (conflicted copy, " + mUsername + " v1).txt"; QCOMPARE( readFileContent( filename ), QByteArray( "remote content" ) ); QCOMPARE( readFileContent( conflictFilename ), QByteArray( "local content" ) ); // Second conflict qDebug() << "modify test1.txt on the server"; - downloadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + downloadRemoteProject( mApiExtra, projectFullName, projectId ); writeFileContent( extraFilename, QByteArray( "remote content 2" ) ); - uploadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + uploadRemoteProject( mApiExtra, projectFullName, projectId ); qDebug() << "modify test1.txt locally and do the sync"; writeFileContent( filename, QByteArray( "local content 2" ) ); @@ -940,13 +944,13 @@ void TestMerginApi::testConflictRemoteUpdateLocalUpdate() // out... in upload's project info handler if there is a need for update, // the upload should be cancelled (or paused to update first). // - downloadRemoteProject( mApi, mWorkspaceName, projectName ); - uploadRemoteProject( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); + uploadRemoteProject( mApi, projectFullName, projectId ); // verify the result: the server version should be in test1.txt // and the local version should go to "test1 (conflicted copy, v).txt" // Note: test1.txt conflict file should still be the same - QString conflictFilename2 = projectDir + "/test1 (conflicted copy, " + mUsername + " v3).txt"; + const QString conflictFilename2 = projectDir + "/test1 (conflicted copy, " + mUsername + " v3).txt"; QCOMPARE( readFileContent( filename ), QByteArray( "remote content 2" ) ); QCOMPARE( readFileContent( conflictFilename ), QByteArray( "local content" ) ); QCOMPARE( readFileContent( conflictFilename2 ), QByteArray( "local content 2" ) ); @@ -954,26 +958,24 @@ void TestMerginApi::testConflictRemoteUpdateLocalUpdate() void TestMerginApi::testConflictRemoteAddLocalAdd() { - // this test downloads a project, creates a new file - // in the meanwhile it creates the same file on the server to create - // a conflict. Finally it tries to upload the local change to test - // the code responsible for conflict resolution (renames the local file) - - QString projectName = "testConflictRemoteAddLocalAdd"; - QString projectDir = mApi->projectsPath() + "/" + projectName; - QString extraProjectDir = mApiExtra->projectsPath() + "/" + projectName; - QString filename = projectDir + "/test-new-file.txt"; - QString extraFilename = extraProjectDir + "/test-new-file.txt"; + const QString projectName = "testConflictRemoteAddLocalAdd"; + const QString projectDir = mApi->projectsPath() + "/" + projectName; + const QString extraProjectDir = mApiExtra->projectsPath() + "/" + projectName; + const QString filename = projectDir + "/test-new-file.txt"; + const QString extraFilename = extraProjectDir + "/test-new-file.txt"; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); + qDebug() << "download initial version"; - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); qDebug() << "create test-new-file.txt on the server"; - downloadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + downloadRemoteProject( mApiExtra, projectFullName, projectId ); writeFileContent( extraFilename, QByteArray( "new remote content" ) ); - uploadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + uploadRemoteProject( mApiExtra, projectFullName, projectId ); qDebug() << "create test-new-file.txt locally and do the sync"; writeFileContent( filename, QByteArray( "new local content" ) ); @@ -983,45 +985,43 @@ void TestMerginApi::testConflictRemoteAddLocalAdd() // out... in upload's project info handler if there is a need for update, // the upload should be cancelled (or paused to update first). // - downloadRemoteProject( mApi, mWorkspaceName, projectName ); - uploadRemoteProject( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); + uploadRemoteProject( mApi, projectFullName, projectId ); // verify the result: the server version should be in test1.txt // and the local version should go to conflicted copy file - QString conflictFilename = projectDir + "/test-new-file (conflicted copy, " + mUsername + " v1).txt"; + const QString conflictFilename = projectDir + "/test-new-file (conflicted copy, " + mUsername + " v1).txt"; QCOMPARE( readFileContent( filename ), QByteArray( "new remote content" ) ); QCOMPARE( readFileContent( conflictFilename ), QByteArray( "new local content" ) ); } void TestMerginApi::testEditConflictScenario() { - // this test simulates creation of edit conflict when two - // clients are trying to update the same attribute. - // edit conflict file should be created inside project folder and synced to server - - QString projectName = "testEditConflictScenario"; - QString projectDir = mApi->projectsPath() + "/" + projectName; - QString extraProjectDir = mApiExtra->projectsPath() + "/" + projectName; + const QString projectName = "testEditConflictScenario"; + const QString projectDir = mApi->projectsPath() + "/" + projectName; + const QString extraProjectDir = mApiExtra->projectsPath() + "/" + projectName; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); // folder rebase_edit_conflict - QString dataProjectDir = TestUtils::testDataDir() + "/" + QStringLiteral( "rebase_edit_conflict" ); + const QString dataProjectDir = TestUtils::testDataDir() + "/" + QStringLiteral( "rebase_edit_conflict" ); qDebug() << "About to create the project"; createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); qDebug() << "Project has been created!"; - QString dbName = QStringLiteral( "data.gpkg" ); - QString baseDB = dataProjectDir + QStringLiteral( "/base.gpkg" ); - QString localChangeDB = dataProjectDir + QStringLiteral( "/local-change.gpkg" ); - QString remoteChangeDB = dataProjectDir + QStringLiteral( "/remote-change.gpkg" ); + const QString dbName = QStringLiteral( "data.gpkg" ); + const QString baseDB = dataProjectDir + QStringLiteral( "/base.gpkg" ); + const QString localChangeDB = dataProjectDir + QStringLiteral( "/local-change.gpkg" ); + const QString remoteChangeDB = dataProjectDir + QStringLiteral( "/remote-change.gpkg" ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); // upload base db InputUtils::copyFile( baseDB, projectDir + "/" + dbName ); - uploadRemoteProject( mApi, mWorkspaceName, projectName ); + uploadRemoteProject( mApi, projectFullName, projectId ); // both clients now sync the project so that both of them have base gpkg - downloadRemoteProject( mApi, mWorkspaceName, projectName ); - downloadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); + downloadRemoteProject( mApiExtra, projectFullName, projectId ); // both clients now make change to the same field InputUtils::removeFile( projectDir + "/" + dbName ); @@ -1030,13 +1030,13 @@ void TestMerginApi::testEditConflictScenario() InputUtils::copyFile( remoteChangeDB, extraProjectDir + "/" + dbName ); // client B syncs his changes - uploadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + uploadRemoteProject( mApiExtra, projectFullName, projectId ); // // now client A syncs, resulting in edit conflict // - uploadRemoteProject( mApi, mWorkspaceName, projectName ); + uploadRemoteProject( mApi, projectFullName, projectId ); QDir projDir( projectDir ); @@ -1044,44 +1044,44 @@ void TestMerginApi::testEditConflictScenario() QVERIFY( InputUtils::fileExists( projectDir + "/" + QString( "data (edit conflict, %1 v2).json" ).arg( mUsername ) ) ); // when client B downloads changes, he should also have that edit conflict file - downloadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + downloadRemoteProject( mApiExtra, projectFullName, projectId ); QVERIFY( InputUtils::fileExists( projectDir + "/" + QString( "data (edit conflict, %1 v2).json" ).arg( mUsername ) ) ); } void TestMerginApi::testUploadWithUpdate() { - // this test triggers the situation when the request to upload a project - // first needs to do an update and only afterwards it uploads changes - - QString projectName = "testUploadWithUpdate"; - QString projectDir = mApi->projectsPath() + "/" + projectName; - QString extraProjectDir = mApiExtra->projectsPath() + "/" + projectName; - QString filenameLocal = projectDir + "/test-new-local-file.txt"; - QString filenameRemote = projectDir + "/test-new-remote-file.txt"; - QString extraFilenameRemote = extraProjectDir + "/test-new-remote-file.txt"; + const QString projectName = "testUploadWithUpdate"; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); + const QString projectDir = mApi->projectsPath() + "/" + projectName; + const QString extraProjectDir = mApiExtra->projectsPath() + "/" + projectName; + const QString filenameLocal = projectDir + "/test-new-local-file.txt"; + const QString filenameRemote = projectDir + "/test-new-remote-file.txt"; + const QString extraFilenameRemote = extraProjectDir + "/test-new-remote-file.txt"; createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); refreshProjectsModel( ProjectsModel::WorkspaceProjectsModel ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); - downloadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); + + downloadRemoteProject( mApiExtra, projectFullName, projectId ); writeFileContent( extraFilenameRemote, QByteArray( "new remote content" ) ); - uploadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + uploadRemoteProject( mApiExtra, projectFullName, projectId ); writeFileContent( filenameLocal, QByteArray( "new local content" ) ); qDebug() << "now do both update + upload"; - uploadRemoteProject( mApi, mWorkspaceName, projectName ); + uploadRemoteProject( mApi, projectFullName, projectId ); QCOMPARE( readFileContent( filenameLocal ), QByteArray( "new local content" ) ); QCOMPARE( readFileContent( filenameRemote ), QByteArray( "new remote content" ) ); // try to re-download the project and see if everything went fine - deleteLocalProject( mApi, mWorkspaceName, projectName ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + deleteLocalProject( mApi, projectId ); + downloadRemoteProject( mApi, projectFullName, projectId ); - Project project1 = mWorkspaceProjectsModel->projectFromId( MerginApi::getFullProjectName( mWorkspaceName, projectName ) ); + Project project1 = mWorkspaceProjectsModel->projectFromId( projectId ); QVERIFY( project1.isLocal() && project1.isMergin() ); QCOMPARE( project1.local.localVersion, 3 ); QCOMPARE( project1.mergin.serverVersion, 3 ); @@ -1093,12 +1093,15 @@ void TestMerginApi::testUploadWithUpdate() void TestMerginApi::testDiffUpload() { - QString projectName = "testDiffUpload"; - QString projectDir = mApi->projectsPath() + "/" + projectName; + const QString projectName = "testDiffUpload"; + const QString projectDir = mApi->projectsPath() + "/" + projectName; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + "diff_project" + "/" ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); + + downloadRemoteProject( mApi, projectFullName, projectId ); QVERIFY( QFileInfo::exists( projectDir + "/.mergin/base.gpkg" ) ); @@ -1111,7 +1114,7 @@ void TestMerginApi::testDiffUpload() QVERIFY( QFile::remove( projectDir + "/base.gpkg" ) ); QVERIFY( QFile::copy( mTestDataPath + "/modified_1_geom.gpkg", projectDir + "/base.gpkg" ) ); - ProjectDiff diff = MerginApi::localProjectChanges( projectDir ); + const ProjectDiff diff = MerginApi::localProjectChanges( projectDir ); ProjectDiff expectedDiff; expectedDiff.localUpdated = QSet() << "base.gpkg"; QVERIFY2( diff == expectedDiff, diff.dump().toStdString().c_str() ); @@ -1119,11 +1122,11 @@ void TestMerginApi::testDiffUpload() GeodiffUtils::ChangesetSummary expectedSummary; expectedSummary["simple"] = GeodiffUtils::TableSummary( 0, 1, 0 ); - QString changes = GeodiffUtils::diffableFilePendingChanges( projectDir, "base.gpkg", true ); - GeodiffUtils::ChangesetSummary summary = GeodiffUtils::parseChangesetSummary( changes ); + const QString changes = GeodiffUtils::diffableFilePendingChanges( projectDir, "base.gpkg", true ); + const GeodiffUtils::ChangesetSummary summary = GeodiffUtils::parseChangesetSummary( changes ); QCOMPARE( summary, expectedSummary ); - uploadRemoteProject( mApi, mWorkspaceName, projectName ); + uploadRemoteProject( mApi, projectFullName, projectId ); QCOMPARE( MerginApi::localProjectChanges( projectDir ), ProjectDiff() ); // no local changes expected QVERIFY( !MerginApi::hasLocalProjectChanges( projectDir, mApi->supportsSelectiveSync() ) ); @@ -1131,12 +1134,15 @@ void TestMerginApi::testDiffUpload() void TestMerginApi::testDiffSubdirsUpload() { - QString projectName = "testDiffSubdirsUpload"; - QString projectDir = mApi->projectsPath() + "/" + projectName; + const QString projectName = "testDiffSubdirsUpload"; + const QString projectDir = mApi->projectsPath() + "/" + projectName; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + "diff_project_subs" + "/" ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); + + downloadRemoteProject( mApi, projectFullName, projectId ); const QString base( "subdir/subsubdir/base.gpkg" ); QVERIFY( QFileInfo::exists( projectDir + "/.mergin/" + base ) ); @@ -1158,11 +1164,11 @@ void TestMerginApi::testDiffSubdirsUpload() GeodiffUtils::ChangesetSummary expectedSummary; expectedSummary["simple"] = GeodiffUtils::TableSummary( 0, 1, 0 ); - QString changes = GeodiffUtils::diffableFilePendingChanges( projectDir, base, true ); + const QString changes = GeodiffUtils::diffableFilePendingChanges( projectDir, base, true ); GeodiffUtils::ChangesetSummary summary = GeodiffUtils::parseChangesetSummary( changes ); QCOMPARE( summary, expectedSummary ); - uploadRemoteProject( mApi, mWorkspaceName, projectName ); + uploadRemoteProject( mApi, projectFullName, projectId ); QCOMPARE( MerginApi::localProjectChanges( projectDir ), ProjectDiff() ); // no local changes expected QVERIFY( !MerginApi::hasLocalProjectChanges( projectDir, mApi->supportsSelectiveSync() ) ); @@ -1170,22 +1176,22 @@ void TestMerginApi::testDiffSubdirsUpload() void TestMerginApi::testDiffUpdateBasic() { - // test case where there is no local change in a gpkg, it is only modified on the server - // and the local file should get the new stuff from server - - QString projectName = "testDiffUpdateBasic"; - QString projectDir = mApi->projectsPath() + "/" + projectName; - QString projectDirExtra = mApiExtra->projectsPath() + "/" + projectName; + const QString projectName = "testDiffUpdateBasic"; + const QString projectDir = mApi->projectsPath() + "/" + projectName; + const QString projectDirExtra = mApiExtra->projectsPath() + "/" + projectName; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + "diff_project" + "/" ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); + + downloadRemoteProject( mApi, projectFullName, projectId ); QVERIFY( QFileInfo::exists( projectDir + "/.mergin/base.gpkg" ) ); QCOMPARE( MerginApi::localProjectChanges( projectDir ), ProjectDiff() ); // no local changes expected QVERIFY( !MerginApi::hasLocalProjectChanges( projectDir, mApi->supportsSelectiveSync() ) ); - QgsVectorLayer *vl0 = new QgsVectorLayer( projectDir + "/base.gpkg|layername=simple", "base", "ogr" ); + const QgsVectorLayer *vl0 = new QgsVectorLayer( projectDir + "/base.gpkg|layername=simple", "base", "ogr" ); QVERIFY( vl0->isValid() ); QCOMPARE( vl0->featureCount(), static_cast( 3 ) ); delete vl0; @@ -1194,20 +1200,20 @@ void TestMerginApi::testDiffUpdateBasic() // download with mApiExtra + modify + upload // - downloadRemoteProject( mApiExtra, mWorkspaceName, projectName ); - bool r0 = QFile::remove( projectDirExtra + "/base.gpkg" ); - bool r1 = QFile::copy( mTestDataPath + "/added_row.gpkg", projectDirExtra + "/base.gpkg" ); + downloadRemoteProject( mApiExtra, projectFullName, projectId ); + const bool r0 = QFile::remove( projectDirExtra + "/base.gpkg" ); + const bool r1 = QFile::copy( mTestDataPath + "/added_row.gpkg", projectDirExtra + "/base.gpkg" ); QVERIFY( r0 && r1 ); - uploadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + uploadRemoteProject( mApiExtra, projectFullName, projectId ); // update our local version now - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); // // check the result // - QgsVectorLayer *vl = new QgsVectorLayer( projectDir + "/base.gpkg|layername=simple", "base", "ogr" ); + const QgsVectorLayer *vl = new QgsVectorLayer( projectDir + "/base.gpkg|layername=simple", "base", "ogr" ); QVERIFY( vl->isValid() ); QCOMPARE( vl->featureCount(), static_cast( 4 ) ); delete vl; @@ -1220,16 +1226,16 @@ void TestMerginApi::testDiffUpdateBasic() void TestMerginApi::testDiffUpdateWithRebase() { - // a test case where there is a local change in a gpkg that is also modified on the server - // and the local change will get rebased on top of the server's change - - QString projectName = "testDiffUpdateWithRebase"; - QString projectDir = mApi->projectsPath() + "/" + projectName; - QString projectDirExtra = mApiExtra->projectsPath() + "/" + projectName; + const QString projectName = "testDiffUpdateWithRebase"; + const QString projectDir = mApi->projectsPath() + "/" + projectName; + const QString projectDirExtra = mApiExtra->projectsPath() + "/" + projectName; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + "diff_project" + "/" ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); + + downloadRemoteProject( mApi, projectFullName, projectId ); QVERIFY( QFileInfo::exists( projectDir + "/.mergin/base.gpkg" ) ); QCOMPARE( MerginApi::localProjectChanges( projectDir ), ProjectDiff() ); // no local changes expected @@ -1239,11 +1245,11 @@ void TestMerginApi::testDiffUpdateWithRebase() // download with mApiExtra + modify + upload // - downloadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + downloadRemoteProject( mApiExtra, projectFullName, projectId ); bool r0 = QFile::remove( projectDirExtra + "/base.gpkg" ); bool r1 = QFile::copy( mTestDataPath + "/added_row.gpkg", projectDirExtra + "/base.gpkg" ); QVERIFY( r0 && r1 ); - uploadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + uploadRemoteProject( mApiExtra, projectFullName, projectId ); // // do a local update of the file @@ -1277,13 +1283,13 @@ void TestMerginApi::testDiffUpdateWithRebase() QCOMPARE( summary, expectedSummary ); // update our local version now - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); // // check the result // - QgsVectorLayer *vl = new QgsVectorLayer( projectDir + "/base.gpkg|layername=simple", "base", "ogr" ); + const QgsVectorLayer *vl = new QgsVectorLayer( projectDir + "/base.gpkg|layername=simple", "base", "ogr" ); QVERIFY( vl->isValid() ); QCOMPARE( vl->featureCount(), static_cast( 5 ) ); delete vl; @@ -1296,19 +1302,16 @@ void TestMerginApi::testDiffUpdateWithRebase() void TestMerginApi::testDiffUpdateWithRebaseFailed() { - // a test case where the local change is something that geodiff does not support - // and thus cannot rebase the changes (should create a conflict file instead) - - // a test case where there is a local change in a gpkg that is also modified on the server - // and the local change will get rebased on top of the server's change - - QString projectName = "testDiffUpdateWithRebaseFailed"; - QString projectDir = mApi->projectsPath() + "/" + projectName; - QString projectDirExtra = mApiExtra->projectsPath() + "/" + projectName; + const QString projectName = "testDiffUpdateWithRebaseFailed"; + const QString projectDir = mApi->projectsPath() + "/" + projectName; + const QString projectDirExtra = mApiExtra->projectsPath() + "/" + projectName; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + "diff_project" + "/" ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); + + downloadRemoteProject( mApi, projectFullName, projectId ); QVERIFY( QFileInfo::exists( projectDir + "/.mergin/base.gpkg" ) ); QCOMPARE( MerginApi::localProjectChanges( projectDir ), ProjectDiff() ); // no local changes expected @@ -1318,11 +1321,11 @@ void TestMerginApi::testDiffUpdateWithRebaseFailed() // download with mApiExtra + modify + upload // - downloadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + downloadRemoteProject( mApiExtra, projectFullName, projectId ); bool r0 = QFile::remove( projectDirExtra + "/base.gpkg" ); bool r1 = QFile::copy( mTestDataPath + "/added_row.gpkg", projectDirExtra + "/base.gpkg" ); QVERIFY( r0 && r1 ); - uploadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + uploadRemoteProject( mApiExtra, projectFullName, projectId ); // // do a local update of the file @@ -1347,11 +1350,11 @@ void TestMerginApi::testDiffUpdateWithRebaseFailed() QSignalSpy spy( mApi, &MerginApi::projectReloadNeededAfterSync ); // update our local version now - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); - // check that projectReloadNeededAfterSync is emited and has correct argument + // check that projectReloadNeededAfterSync is emitted and has correct argument QCOMPARE( spy.count(), 1 ); - QCOMPARE( spy.takeFirst().at( 0 ).toString(), mWorkspaceName + "/" + projectName ); + QCOMPARE( spy.takeFirst().at( 0 ).toString(), projectId ); // // check the result @@ -1369,16 +1372,16 @@ void TestMerginApi::testDiffUpdateWithRebaseFailed() void TestMerginApi::testUpdateWithDiffs() { - // a test case where we download initial version (v1), then there will be - // two versions with diffs (v2 and v3), afterwards we try to update the local project. - - QString projectName = "testUpdateWithDiffs"; - QString projectDir = mApi->projectsPath() + "/" + projectName; - QString projectDirExtra = mApiExtra->projectsPath() + "/" + projectName; + const QString projectName = "testUpdateWithDiffs"; + const QString projectDir = mApi->projectsPath() + "/" + projectName; + const QString projectDirExtra = mApiExtra->projectsPath() + "/" + projectName; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + "diff_project" + "/" ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); + + downloadRemoteProject( mApi, projectFullName, projectId ); QVERIFY( QFileInfo::exists( projectDir + "/.mergin/base.gpkg" ) ); QCOMPARE( MerginApi::localProjectChanges( projectDir ), ProjectDiff() ); // no local changes expected @@ -1388,26 +1391,26 @@ void TestMerginApi::testUpdateWithDiffs() // download with mApiExtra + modify + upload // - downloadRemoteProject( mApiExtra, mWorkspaceName, projectName ); - bool r0 = QFile::remove( projectDirExtra + "/base.gpkg" ); - bool r1 = QFile::copy( mTestDataPath + "/added_row.gpkg", projectDirExtra + "/base.gpkg" ); + downloadRemoteProject( mApiExtra, projectFullName, projectId ); + const bool r0 = QFile::remove( projectDirExtra + "/base.gpkg" ); + const bool r1 = QFile::copy( mTestDataPath + "/added_row.gpkg", projectDirExtra + "/base.gpkg" ); QVERIFY( r0 && r1 ); - uploadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + uploadRemoteProject( mApiExtra, projectFullName, projectId ); // one more change + upload - bool r2 = QFile::remove( projectDirExtra + "/base.gpkg" ); - bool r3 = QFile::copy( mTestDataPath + "/added_row_2.gpkg", projectDirExtra + "/base.gpkg" ); + const bool r2 = QFile::remove( projectDirExtra + "/base.gpkg" ); + const bool r3 = QFile::copy( mTestDataPath + "/added_row_2.gpkg", projectDirExtra + "/base.gpkg" ); QVERIFY( r2 && r3 ); writeFileContent( projectDirExtra + "/dummy.txt", "first" ); - uploadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + uploadRemoteProject( mApiExtra, projectFullName, projectId ); // // now update project locally // - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); - QgsVectorLayer *vl = new QgsVectorLayer( projectDir + "/base.gpkg|layername=simple", "base", "ogr" ); + const QgsVectorLayer *vl = new QgsVectorLayer( projectDir + "/base.gpkg|layername=simple", "base", "ogr" ); QVERIFY( vl->isValid() ); QCOMPARE( vl->featureCount(), static_cast( 5 ) ); delete vl; @@ -1419,38 +1422,30 @@ void TestMerginApi::testUpdateWithDiffs() void TestMerginApi::testUpdateWithMissedVersion() { - // when updating from v3 to v4, it is expected that we will get references to diffs for v2-v3 and v3-v4. - // There was a bug where we always ignored the first one. But it could happen that there is no update in v2-v3, - // and we ended up ignoring v3-v4, ending up with broken basefiles. - - // 1. [extra] create project, upload .gpkg (v1) - // 3. [extra] upload a new file (v2) - // 2. [main] download project (v2) - // 4. [extra] upload updated .gpkg (v3) - // 5. [main] update from v2 to v3 - - QString projectName = "testUpdateWithMissedVersion"; - QString projectDir = mApi->projectsPath() + "/" + projectName; - QString projectDirExtra = mApiExtra->projectsPath() + "/" + projectName; - + const QString projectName = "testUpdateWithMissedVersion"; + const QString projectDir = mApi->projectsPath() + "/" + projectName; + const QString projectDirExtra = mApiExtra->projectsPath() + "/" + projectName; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); // step 1 createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + "diff_project" + "/" ); + // step 2 - downloadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApiExtra, projectFullName, projectId ); writeFileContent( projectDirExtra + "/file1.txt", QByteArray( "hello" ) ); - uploadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + uploadRemoteProject( mApiExtra, projectFullName, projectId ); // step 3 - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); // step 4 - bool r0 = QFile::remove( projectDirExtra + "/base.gpkg" ); - bool r1 = QFile::copy( mTestDataPath + "/added_row.gpkg", projectDirExtra + "/base.gpkg" ); + const bool r0 = QFile::remove( projectDirExtra + "/base.gpkg" ); + const bool r1 = QFile::copy( mTestDataPath + "/added_row.gpkg", projectDirExtra + "/base.gpkg" ); QVERIFY( r0 && r1 ); - uploadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + uploadRemoteProject( mApiExtra, projectFullName, projectId ); // step 5 - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); // check that added row in v3 has been added in our local file too - QgsVectorLayer *vl = new QgsVectorLayer( projectDir + "/base.gpkg|layername=simple", "base", "ogr" ); + const QgsVectorLayer *vl = new QgsVectorLayer( projectDir + "/base.gpkg|layername=simple", "base", "ogr" ); QVERIFY( vl->isValid() ); QCOMPARE( vl->featureCount(), static_cast( 4 ) ); delete vl; @@ -1458,59 +1453,56 @@ void TestMerginApi::testUpdateWithMissedVersion() void TestMerginApi::testMigrateProject() { - QString projectName = "testMigrateProject"; + const QString projectName = "testMigrateProject"; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); // clean leftovers from previous tests deleteRemoteProjectNow( mApi, mWorkspaceName, projectName ); // make local copy of project - QString projectDir = mApi->projectsPath() + "/" + projectName; + const QString projectDir = mApi->projectsPath() + "/" + projectName; createLocalProject( projectDir ); - // reload localmanager after copying the project + // reload local manager after copying the project mApi->mLocalProjects.reloadDataDir(); - QStringList entryList = QDir( projectDir ).entryList( QDir::NoDotAndDotDot | QDir::Dirs ); + const QStringList entryList = QDir( projectDir ).entryList( QDir::NoDotAndDotDot | QDir::Dirs ); // migrate project QSignalSpy spy( mApi, &MerginApi::projectCreated ); + QSignalSpy spy1( mApi, &MerginApi::projectAttachedToMergin ); QSignalSpy spy2( mApi, &MerginApi::syncProjectFinished ); - mApi->migrateProjectToMergin( projectName, mWorkspaceName ); + // we pass only project name to search for as the project is local and doesn't have workspace set yet + QString localProjectId = TestUtils::findProjectByName( projectName, mApi->mLocalProjects.projects().values() ).id(); + mApi->createProject( mWorkspaceName, projectName, localProjectId ); QVERIFY( spy.wait( TestUtils::LONG_REPLY ) ); - QCOMPARE( spy.count(), 1 ); + QCOMPARE( spy.count(), 2 ); QCOMPARE( spy.takeFirst().at( 1 ).toBool(), true ); + QVERIFY( spy1.wait( TestUtils::LONG_REPLY ) ); QCOMPARE( mApi->transactions().count(), 1 ); QVERIFY( spy2.wait( TestUtils::LONG_REPLY * 5 ) ); + // project ID changed after the push to server + localProjectId = TestUtils::findProjectByName( projectFullName, mApi->mLocalProjects.projects().values() ).id(); // remove local copy of project - deleteLocalProject( mApi, mWorkspaceName, projectName ); + deleteLocalProject( mApi, localProjectId ); QVERIFY( !QFileInfo::exists( projectDir ) ); // download the project - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, localProjectId ); // verify that all files have been uploaded - QStringList entryList2 = QDir( projectDir ).entryList( QDir::NoDotAndDotDot | QDir::Dirs ); + const QStringList entryList2 = QDir( projectDir ).entryList( QDir::NoDotAndDotDot | QDir::Dirs ); QCOMPARE( entryList, entryList2 ); } void TestMerginApi::testMigrateProjectAndSync() { - // When a new project is migrated to Mergin, creating basefiles for diffable files was omitted. - // Therefore sync was not properly working resulting into having a conflict file. - // Test covers creating a new project, migrating it to Mergin and both sides sync. - - // 1. [main] create project with .gpkg (v1) file - // 2. [main] migrate the project to mergin - // 3. [extra] download the project and make changes to .gpkg (v2) - // 4. [main] sync the project (v2), should be valid without conflicts - // 5. [main] make changes to .gpkg (v3) and sync - // 6. [extra] sync the project (v3), should be valid without conflicts - - QString projectName = "testMigrateProjectAndSync"; - QString projectDir = mApi->projectsPath() + "/" + projectName; - QString projectDirExtra = mApiExtra->projectsPath() + "/" + projectName; + const QString projectName = "testMigrateProjectAndSync"; + const QString projectDir = mApi->projectsPath() + "/" + projectName; + const QString projectDirExtra = mApiExtra->projectsPath() + "/" + projectName; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); // clean leftovers from previous tests deleteRemoteProjectNow( mApi, mWorkspaceName, projectName ); @@ -1518,27 +1510,32 @@ void TestMerginApi::testMigrateProjectAndSync() // step 1 createLocalProject( projectDir ); mApi->mLocalProjects.reloadDataDir(); + QString localProjectId = TestUtils::findProjectByName( projectName, mApi->mLocalProjects.projects().values() ).id(); // step 2 QSignalSpy spy( mApi, &MerginApi::projectCreated ); + QSignalSpy spy1( mApi, &MerginApi::projectAttachedToMergin ); QSignalSpy spy2( mApi, &MerginApi::syncProjectFinished ); - mApi->migrateProjectToMergin( projectName, mWorkspaceName ); + mApi->createProject( mWorkspaceName, projectName, localProjectId ); QVERIFY( spy.wait( TestUtils::LONG_REPLY ) ); - QCOMPARE( spy.count(), 1 ); + QCOMPARE( spy.count(), 2 ); QCOMPARE( spy.takeFirst().at( 1 ).toBool(), true ); + QVERIFY( spy1.wait( TestUtils::LONG_REPLY ) ); QCOMPARE( mApi->transactions().count(), 1 ); QVERIFY( spy2.wait( TestUtils::LONG_REPLY * 5 ) ); + localProjectId = TestUtils::findProjectByName( projectFullName, mApi->mLocalProjects.projects().values() ).id(); + // step 3 - downloadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + downloadRemoteProject( mApiExtra, projectFullName, localProjectId ); bool r0 = QFile::remove( projectDirExtra + "/base.gpkg" ); bool r1 = QFile::copy( mTestDataPath + "/added_row.gpkg", projectDirExtra + "/base.gpkg" ); QVERIFY( r0 && r1 ); - uploadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + uploadRemoteProject( mApiExtra, projectFullName, localProjectId ); // step 4 - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, localProjectId ); QVERIFY( QFile( projectDir + "/base.gpkg" ).exists() ); QStringList projectMerginDirEntries = QDir( projectDir + "/.mergin" ).entryList( QDir::AllEntries | QDir::NoDotAndDotDot ); for ( QString filepath : projectMerginDirEntries ) @@ -1550,10 +1547,10 @@ void TestMerginApi::testMigrateProjectAndSync() r0 = QFile::remove( projectDir + "/base.gpkg" ); r1 = QFile::copy( mTestDataPath + "/added_row_2.gpkg", projectDir + "/base.gpkg" ); QVERIFY( r0 && r1 ); - uploadRemoteProject( mApi, mWorkspaceName, projectName ); + uploadRemoteProject( mApi, projectFullName, localProjectId ); // step 6 - downloadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + downloadRemoteProject( mApiExtra, projectFullName, localProjectId ); QVERIFY( QFile( projectDir + "/base.gpkg" ).exists() ); QStringList projectMerginDirExtraEntries = QDir( projectDirExtra + "/.mergin" ).entryList( QDir::AllEntries | QDir::NoDotAndDotDot ); for ( QString filepath : projectMerginDirExtraEntries ) @@ -1564,59 +1561,65 @@ void TestMerginApi::testMigrateProjectAndSync() void TestMerginApi::testMigrateDetachProject() { - QString projectName = "testMigrateDetachProject"; + const QString projectName = "testMigrateDetachProject"; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); // clean leftovers from previous tests deleteRemoteProjectNow( mApi, mWorkspaceName, projectName ); // make local copy of project - QString projectDir = mApi->projectsPath() + "/" + projectName; + const QString projectDir = mApi->projectsPath() + "/" + projectName; createLocalProject( projectDir ); - // reload localmanager after copying the project + // reload local manager after copying the project mApi->mLocalProjects.reloadDataDir(); + QString localProjectId = TestUtils::findProjectByName( projectName, mApi->mLocalProjects.projects().values() ).id(); // migrate project QSignalSpy spy( mApi, &MerginApi::projectCreated ); + QSignalSpy spy1( mApi, &MerginApi::projectAttachedToMergin ); QSignalSpy spy2( mApi, &MerginApi::syncProjectFinished ); - mApi->migrateProjectToMergin( projectName, mWorkspaceName ); + mApi->createProject( mWorkspaceName, projectName, localProjectId ); QVERIFY( spy.wait( TestUtils::LONG_REPLY ) ); - QCOMPARE( spy.count(), 1 ); + QCOMPARE( spy.count(), 2 ); QCOMPARE( spy.takeFirst().at( 1 ).toBool(), true ); + QVERIFY( spy1.wait( TestUtils::LONG_REPLY ) ); QCOMPARE( mApi->transactions().count(), 1 ); QVERIFY( spy2.wait( TestUtils::LONG_REPLY * 5 ) ); + localProjectId = TestUtils::findProjectByName( projectFullName, mApi->mLocalProjects.projects().values() ).id(); + // TEST if is mergin project QVERIFY( QFileInfo::exists( projectDir + "/.mergin/" ) ); // detach project - QString projectNamespace = mWorkspaceName; - mApi->detachProjectFromMergin( projectNamespace, projectName ); + mApi->detachProjectFromMergin( localProjectId ); // TEST if is NOT mergin project QVERIFY( !QFileInfo::exists( projectDir + "/.mergin/" ) ); } void TestMerginApi::testSelectiveSync() { - // Case: Clients have following configuration: selective sync on, selective-sync-dir empty (project dir by default) - // Action 1: Client 1 uploads some images and Client 2 sync without downloading the images - // Action 2: Client 2 uploads an image and do not remove not-synced images. Client 1 syncs without downloading the image, still having own images. - // Create a project - QString projectName = "testSelectiveSync"; - QString projectDir = mApi->projectsPath() + "/" + projectName; - QString projectDirExtra = mApiExtra->projectsPath() + "/" + projectName; + const QString projectName = "testSelectiveSync"; + const QString projectDir = mApi->projectsPath() + "/" + projectName; + const QString projectDirExtra = mApiExtra->projectsPath() + "/" + projectName; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); // Create photo files - QDir dir; - QString photoPath( projectDir + "/subdir" ); + const QDir dir; + const QString photoPath( projectDir + "/subdir" ); if ( !dir.exists( photoPath ) ) - dir.mkpath( photoPath ); + if ( !dir.mkpath( photoPath ) ) + { + qDebug() << QString( "Failed to create directory for %1" ).arg( photoPath ); + }; QFile file( projectDir + "/" + "photo.jpg" ); file.open( QIODevice::WriteOnly ); @@ -1625,23 +1628,23 @@ void TestMerginApi::testSelectiveSync() file1.open( QIODevice::WriteOnly ); // Download the project and copy mergin config file containing selective sync properties - downloadRemoteProject( mApiExtra, mWorkspaceName, projectName ); - QString configFilePathExtra( projectDirExtra + "/mergin-config.json" ); + downloadRemoteProject( mApiExtra, projectFullName, projectId ); + const QString configFilePathExtra( projectDirExtra + "/mergin-config.json" ); QVERIFY( QFile::copy( mTestDataPath + "/mergin-config-project-dir.json", configFilePathExtra ) ); // Upload config file - uploadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + uploadRemoteProject( mApiExtra, projectFullName, projectId ); // Sync event 1: // Client 1 uploads images - uploadRemoteProject( mApi, mWorkspaceName, projectName ); + uploadRemoteProject( mApi, projectFullName, projectId ); // Download project and check - downloadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + downloadRemoteProject( mApiExtra, projectFullName, projectId ); - QFile fileExtra( projectDirExtra + "/photo.jpg" ); + const QFile fileExtra( projectDirExtra + "/photo.jpg" ); QVERIFY( !fileExtra.exists() ); - QFile fileExtra1( projectDirExtra + "/subdir/photo.jpg" ); + const QFile fileExtra1( projectDirExtra + "/subdir/photo.jpg" ); QVERIFY( !fileExtra1.exists() ); // Sync event 2: @@ -1651,48 +1654,37 @@ void TestMerginApi::testSelectiveSync() fileExtra2.open( QIODevice::WriteOnly ); // Client 2 uploads a new image - uploadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + uploadRemoteProject( mApiExtra, projectFullName, projectId ); // Client 1 syncs without Client 2's new image and without removing own images. - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); QVERIFY( file.exists() ); QVERIFY( file1.exists() ); - QFile file2( projectDir + "/" + "photoExtra.png" ); + const QFile file2( projectDir + "/" + "photoExtra.png" ); QVERIFY( !file2.exists() ); } void TestMerginApi::testSelectiveSyncSubfolder() { - /* - * Case: Downloading project with config. - * - * We have following scenario: - * { - * "input-selective-sync": true, - * "input-selective-sync-dir": "photos" // having subfolder - * } - * - * Action 1: Client 1 creates project with mergin-config and uploads some images, - * Client 2 should sync without downloading the images. - * Action 2: Client 2 uploads two images, one in "photos" subdirectory and second in project root. - * Client 1 should sync without downloading the image in "photos" subdirectory and should still have own images - * (they should not be deleted even though Client 2 did not have them when syncing) - */ - // Create a project - QString projectName = "testSelectiveSyncSubfolder"; - QString projectDir = mApi->projectsPath() + "/" + projectName; - QString projectDirExtra = mApiExtra->projectsPath() + "/" + projectName; + const QString projectName = "testSelectiveSyncSubfolder"; + const QString projectDir = mApi->projectsPath() + "/" + projectName; + const QString projectDirExtra = mApiExtra->projectsPath() + "/" + projectName; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); createRemoteProject( mApi, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); // Create photo files - QDir dir; - QString photoPath( projectDir + "/photos" ); + const QDir dir; + const QString photoPath( projectDir + "/photos" ); if ( !dir.exists( photoPath ) ) - dir.mkpath( photoPath ); + if ( !dir.mkpath( photoPath ) ) + { + qDebug() << QString( "Failed to create directory %1" ).arg( photoPath ); + }; QFile file( photoPath + "/" + "photoA.jpg" ); file.open( QIODevice::WriteOnly ); @@ -1703,21 +1695,21 @@ void TestMerginApi::testSelectiveSyncSubfolder() file1.close(); // Add mergin-config.json to the project - QString configFilePath( projectDir + "/mergin-config.json" ); + const QString configFilePath( projectDir + "/mergin-config.json" ); QVERIFY( QFile::copy( mTestDataPath + "/mergin-config-subfolder.json", configFilePath ) ); // Upload project - uploadRemoteProject( mApi, mWorkspaceName, projectName ); + uploadRemoteProject( mApi, projectFullName, projectId ); // Client 2 in Action 1: should download project without images in subfolder "photos" - downloadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + downloadRemoteProject( mApiExtra, projectFullName, projectId ); - QString photoPathExtra( projectDirExtra + "/photos" ); + const QString photoPathExtra( projectDirExtra + "/photos" ); - QFile fileExtra( photoPathExtra + "/" + "photoA.jpg" ); + const QFile fileExtra( photoPathExtra + "/" + "photoA.jpg" ); QVERIFY( !fileExtra.exists() ); - QFile fileExtra1( photoPathExtra + "/" + "photoB.png" ); + const QFile fileExtra1( photoPathExtra + "/" + "photoB.png" ); QVERIFY( !fileExtra1.exists() ); // ---- @@ -1725,9 +1717,12 @@ void TestMerginApi::testSelectiveSyncSubfolder() // ---- // Client 2 adds 2 images, one to project root, another to "photos" subfolder - QDir photoDirExtra( photoPathExtra ); + const QDir photoDirExtra( photoPathExtra ); if ( !photoDirExtra.exists() ) // if the subfolder contained only photos, it was not even created in Client 2 - photoDirExtra.mkpath( photoPathExtra ); + if ( !photoDirExtra.mkpath( photoPathExtra ) ) + { + qDebug() << QString( "Failed to create directory for %1" ).arg( photoPathExtra ); + }; QFile extraFile( photoPathExtra + "/" + "photoC.jpg" ); extraFile.open( QIODevice::WriteOnly ); @@ -1738,47 +1733,44 @@ void TestMerginApi::testSelectiveSyncSubfolder() extraRootFile.close(); // Client 2 uploads, Client 1 downloads - uploadRemoteProject( mApiExtra, mWorkspaceName, projectName ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + uploadRemoteProject( mApiExtra, projectFullName, projectId ); + downloadRemoteProject( mApi, projectFullName, projectId ); // Check existence of "photoD" in root and not existence of photoC in "photos" - QFile fileExtra2( photoPath + "/" + "photoC.jpg" ); + const QFile fileExtra2( photoPath + "/" + "photoC.jpg" ); QVERIFY( !fileExtra2.exists() ); - QFile fileExtra3( projectDir + "/" + "photoD.png" ); + const QFile fileExtra3( projectDir + "/" + "photoD.png" ); QVERIFY( fileExtra3.exists() ); // Check that photos were not deleted for Client 1 - QFile file2( photoPath + "/" + "photoA.jpg" ); + const QFile file2( photoPath + "/" + "photoA.jpg" ); QVERIFY( file2.exists() ); - QFile file3( photoPath + "/" + "photoB.png" ); - QVERIFY( file2.exists() ); + const QFile file3( photoPath + "/" + "photoB.png" ); + QVERIFY( file3.exists() ); } void TestMerginApi::testSelectiveSyncAddConfigToExistingProject() { - /* - * Case: Have a project with photos without mergin config, add it when both clients are using project already to simulate - * users that add mergin config to existing projects. - * - * Procedure: Create project with photos, sync it to both clients, then let Client 1 add mergin config together with several - * pictures and see if the new pictures are NOT synced. - */ - // Create a project - QString projectName = "testSelectiveSyncAddConfigToExistingProject"; - QString projectDir = mApi->projectsPath() + "/" + projectName; - QString projectDirExtra = mApiExtra->projectsPath() + "/" + projectName; + const QString projectName = "testSelectiveSyncAddConfigToExistingProject"; + const QString projectDir = mApi->projectsPath() + "/" + projectName; + const QString projectDirExtra = mApiExtra->projectsPath() + "/" + projectName; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); createRemoteProject( mApi, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); // Create photo files - QDir dir; - QString photoPath( projectDir + "/photos" ); + const QDir dir; + const QString photoPath( projectDir + "/photos" ); if ( !dir.exists( photoPath ) ) - dir.mkpath( photoPath ); + if ( !dir.mkpath( photoPath ) ) + { + qDebug() << QString( "Failed to create directory %1" ).arg( photoPath ); + }; QFile file( photoPath + "/" + "photoA.jpg" ); file.open( QIODevice::WriteOnly ); @@ -1789,19 +1781,19 @@ void TestMerginApi::testSelectiveSyncAddConfigToExistingProject() file1.close(); // Sync project for both clients, Client 2 should have both pictures - uploadRemoteProject( mApi, mWorkspaceName, projectName ); - downloadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + uploadRemoteProject( mApi, projectFullName, projectId ); + downloadRemoteProject( mApiExtra, projectFullName, projectId ); - QString photoPathExtra( projectDirExtra + "/photos" ); + const QString photoPathExtra( projectDirExtra + "/photos" ); - QFile fileExtra( photoPathExtra + "/" + "photoA.jpg" ); + const QFile fileExtra( photoPathExtra + "/" + "photoA.jpg" ); QVERIFY( fileExtra.exists() ); - QFile fileExtra1( photoPathExtra + "/" + "photoB.png" ); + const QFile fileExtra1( photoPathExtra + "/" + "photoB.png" ); QVERIFY( fileExtra1.exists() ); // Add mergin-config.json to the project together with another image - QString configFilePath( projectDir + "/mergin-config.json" ); + const QString configFilePath( projectDir + "/mergin-config.json" ); QVERIFY( QFile::copy( mTestDataPath + "/mergin-config-subfolder.json", configFilePath ) ); QFile file2( photoPath + "/" + "photoC.png" ); @@ -1809,47 +1801,48 @@ void TestMerginApi::testSelectiveSyncAddConfigToExistingProject() file2.close(); // Sync project for both clients - uploadRemoteProject( mApi, mWorkspaceName, projectName ); - downloadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + uploadRemoteProject( mApi, projectFullName, projectId ); + downloadRemoteProject( mApiExtra, projectFullName, projectId ); // With mergin-config, "photoC" should not exist for Client 2 - QFile fileExtra2( photoPathExtra + "/" + "photoC.png" ); + const QFile fileExtra2( photoPathExtra + "/" + "photoC.png" ); QVERIFY( !fileExtra2.exists() ); } void TestMerginApi::testSelectiveSyncRemoveConfig() { - /* - * Case: Remove mergin-config from an existing project with photos. - * - * We will create another API client that will serve as a server mirror, it will not use selective sync, - * but will simulate as if someone manipulated project from browser via Mergin - */ - - QString serverMirrorDataPath = mApi->projectsPath() + "/" + "serverMirror"; - QDir serverMirrorDataDir( serverMirrorDataPath ); + const QString serverMirrorDataPath = mApi->projectsPath() + "/" + "serverMirror"; + const QDir serverMirrorDataDir( serverMirrorDataPath ); if ( !serverMirrorDataDir.exists() ) - serverMirrorDataDir.mkpath( serverMirrorDataPath ); + if ( !serverMirrorDataDir.mkpath( serverMirrorDataPath ) ) + { + qDebug() << QString( "Failed to create directory for %1" ).arg( serverMirrorDataPath ); + }; LocalProjectsManager *serverMirrorProjects = new LocalProjectsManager( serverMirrorDataPath + "/" ); MerginApi *serverMirror = new MerginApi( *serverMirrorProjects, this ); serverMirror->setSupportsSelectiveSync( false ); // Create a project with photos and mergin-config - QString projectName = "testSelectiveSyncRemoveConfig"; + const QString projectName = "testSelectiveSyncRemoveConfig"; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); - QString projectClient1 = mApi->projectsPath() + "/" + projectName; - QString projectClient2 = mApiExtra->projectsPath() + "/" + projectName; - QString projectServer = serverMirror->projectsPath() + "/" + projectName; + const QString projectClient1 = mApi->projectsPath() + "/" + projectName; + const QString projectClient2 = mApiExtra->projectsPath() + "/" + projectName; + const QString projectServer = serverMirror->projectsPath() + "/" + projectName; createRemoteProject( mApi, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); // Create photo files - QDir dir; - QString photoPathClient1( projectClient1 + "/photos" ); + const QDir dir; + const QString photoPathClient1( projectClient1 + "/photos" ); if ( !dir.exists( photoPathClient1 ) ) - dir.mkpath( photoPathClient1 ); + if ( !dir.mkpath( photoPathClient1 ) ) + { + qDebug() << QString( "Failed to create directory for %1" ).arg( photoPathClient1 ); + }; QFile file( photoPathClient1 + "/" + "photoA.jpg" ); file.open( QIODevice::WriteOnly ); @@ -1859,8 +1852,8 @@ void TestMerginApi::testSelectiveSyncRemoveConfig() file1.open( QIODevice::WriteOnly ); file1.close(); - uploadRemoteProject( mApi, mWorkspaceName, projectName ); - downloadRemoteProject( serverMirror, mWorkspaceName, projectName ); + uploadRemoteProject( mApi, projectFullName, projectId ); + downloadRemoteProject( serverMirror, projectFullName, projectId ); QString configFilePath = projectServer + "/" + "mergin-config.json"; QVERIFY( createJsonFile( configFilePath, @@ -1869,8 +1862,8 @@ void TestMerginApi::testSelectiveSyncRemoveConfig() { "input-selective-sync-dir", "photos" } } ) ); - uploadRemoteProject( serverMirror, mWorkspaceName, projectName ); - downloadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + uploadRemoteProject( serverMirror, projectFullName, projectId ); + downloadRemoteProject( mApiExtra, projectFullName, projectId ); QString photoPathClient2( projectClient2 + "/photos" ); @@ -1883,37 +1876,39 @@ void TestMerginApi::testSelectiveSyncRemoveConfig() // Client 2 adds another picture QDir photoDirExtra( photoPathClient2 ); if ( !photoDirExtra.exists() ) - photoDirExtra.mkpath( photoPathClient2 ); + if ( !photoDirExtra.mkpath( photoPathClient2 ) ) + { + qDebug() << QString( "Failed to create directory for %1" ).arg( photoPathClient2 ); + }; QFile file2( photoPathClient2 + "/" + "photoC.png" ); file2.open( QIODevice::WriteOnly ); file2.close(); - uploadRemoteProject( mApiExtra, mWorkspaceName, projectName ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); - downloadRemoteProject( serverMirror, mWorkspaceName, projectName ); + uploadRemoteProject( mApiExtra, projectFullName, projectId ); + downloadRemoteProject( mApi, projectFullName, projectId ); + downloadRemoteProject( serverMirror, projectFullName, projectId ); // Let's remove mergin config InputUtils::removeFile( configFilePath ); QVERIFY( !InputUtils::fileExists( configFilePath ) ); // Sync removed config - uploadRemoteProject( serverMirror, mWorkspaceName, projectName ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); // download back to apply the changes -> should download photos + uploadRemoteProject( serverMirror, projectFullName, projectId ); + downloadRemoteProject( mApi, projectFullName, projectId ); // download back to apply the changes -> should download photos QFile fextra( photoPathClient2 + "/" + "photoC2-extra.png" ); fextra.open( QIODevice::WriteOnly ); fextra.close(); - uploadRemoteProject( mApiExtra, mWorkspaceName, projectName ); - downloadRemoteProject( serverMirror, mWorkspaceName, projectName ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + uploadRemoteProject( mApiExtra, projectFullName, projectId ); + downloadRemoteProject( serverMirror, projectFullName, projectId ); + downloadRemoteProject( mApi, projectFullName, projectId ); - QString photoPathServer = serverMirrorDataPath + "/" + projectName + "/" + "photos"; + const QString photoPathServer = serverMirrorDataPath + "/" + projectName + "/" + "photos"; // check that all clients have all photos - QStringList photos; - photos << "photoA.jpg" << "photoB.png" << "photoC.png" << "photoC2-extra.png"; + const QStringList photos( { "photoA.jpg", "photoB.png", "photoC.png", "photoC2-extra.png" } ); for ( const QString &photo : photos ) { QFile photo1( photoPathClient1 + "/" + photo ); @@ -1931,36 +1926,38 @@ void TestMerginApi::testSelectiveSyncRemoveConfig() void TestMerginApi::testSelectiveSyncDisabledInConfig() { - /* - * Case: Disable selective sync in mergin-config in an existing project with photos and selective sync previously enabled. - * - * We will create another API client that will serve as a server mirror, it will not use selective sync, - * but will simulate as if someone manipulated project from browser via Mergin - */ - QString serverMirrorDataPath = mApi->projectsPath() + "/" + "serverMirror"; - QDir serverMirrorDataDir( serverMirrorDataPath ); + const QString serverMirrorDataPath = mApi->projectsPath() + "/" + "serverMirror"; + const QDir serverMirrorDataDir( serverMirrorDataPath ); if ( !serverMirrorDataDir.exists() ) - serverMirrorDataDir.mkpath( serverMirrorDataPath ); + if ( !serverMirrorDataDir.mkpath( serverMirrorDataPath ) ) + { + qDebug() << QString( "Failed to create directory for %1" ).arg( serverMirrorDataPath ); + }; LocalProjectsManager *serverMirrorProjects = new LocalProjectsManager( serverMirrorDataPath + "/" ); MerginApi *serverMirror = new MerginApi( *serverMirrorProjects, this ); serverMirror->setSupportsSelectiveSync( false ); // Create a project with photos and mergin-config - QString projectName = "testSelectiveSyncDisabledInConfig"; + const QString projectName = "testSelectiveSyncDisabledInConfig"; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); - QString projectClient1 = mApi->projectsPath() + "/" + projectName; - QString projectClient2 = mApiExtra->projectsPath() + "/" + projectName; - QString projectServer = serverMirror->projectsPath() + "/" + projectName; + const QString projectClient1 = mApi->projectsPath() + "/" + projectName; + const QString projectClient2 = mApiExtra->projectsPath() + "/" + projectName; + const QString projectServer = serverMirror->projectsPath() + "/" + projectName; createRemoteProject( mApi, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); // Create photo files QDir dir; QString photoPathClient1( projectClient1 + "/" + "photos" ); if ( !dir.exists( photoPathClient1 ) ) - dir.mkpath( photoPathClient1 ); + if ( !dir.mkpath( photoPathClient1 ) ) + { + qDebug() << QString( "Failed to create directory for %1" ).arg( photoPathClient1 ); + }; QFile file( photoPathClient1 + "/" + "photoA.jpg" ); file.open( QIODevice::WriteOnly ); @@ -1970,8 +1967,8 @@ void TestMerginApi::testSelectiveSyncDisabledInConfig() file1.open( QIODevice::WriteOnly ); file1.close(); - uploadRemoteProject( mApi, mWorkspaceName, projectName ); - downloadRemoteProject( serverMirror, mWorkspaceName, projectName ); + uploadRemoteProject( mApi, projectFullName, projectId ); + downloadRemoteProject( serverMirror, projectFullName, projectId ); QString configFilePath = projectServer + "/" + "mergin-config.json"; QVERIFY( createJsonFile( configFilePath, @@ -1980,8 +1977,8 @@ void TestMerginApi::testSelectiveSyncDisabledInConfig() { "input-selective-sync-dir", "photos" } } ) ); - uploadRemoteProject( serverMirror, mWorkspaceName, projectName ); - downloadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + uploadRemoteProject( serverMirror, projectFullName, projectId ); + downloadRemoteProject( mApiExtra, projectFullName, projectId ); QString photoPathClient2( projectClient2 + "/photos" ); @@ -1993,7 +1990,10 @@ void TestMerginApi::testSelectiveSyncDisabledInConfig() QDir photoDirExtra( photoPathClient2 ); if ( !photoDirExtra.exists() ) - photoDirExtra.mkpath( photoPathClient2 ); + if ( !photoDirExtra.mkpath( photoPathClient2 ) ) + { + qDebug() << QString( "Failed to create directory for %1" ).arg( photoPathClient2 ); + }; // simulate some traffic, let both clients create few photos several times (so that project has longer history) for ( int i : { 1, 2, 3, 4, 5 } ) @@ -2006,11 +2006,11 @@ void TestMerginApi::testSelectiveSyncDisabledInConfig() f2.open( QIODevice::WriteOnly ); f2.close(); - uploadRemoteProject( mApiExtra, mWorkspaceName, projectName ); - uploadRemoteProject( mApi, mWorkspaceName, projectName ); + uploadRemoteProject( mApiExtra, projectFullName, projectId ); + uploadRemoteProject( mApi, projectFullName, projectId ); } - downloadRemoteProject( serverMirror, mWorkspaceName, projectName ); + downloadRemoteProject( serverMirror, projectFullName, projectId ); // Let's disable selective sync InputUtils::removeFile( configFilePath ); @@ -2023,20 +2023,19 @@ void TestMerginApi::testSelectiveSyncDisabledInConfig() } ) ); // Sync changed config - uploadRemoteProject( serverMirror, mWorkspaceName, projectName ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); // download back to apply the changes -> should download photos + uploadRemoteProject( serverMirror, projectFullName, projectId ); + downloadRemoteProject( mApi, projectFullName, projectId ); // download back to apply the changes -> should download photos QFile fextra( photoPathClient2 + "/" + "photoC2-extra.png" ); fextra.open( QIODevice::WriteOnly ); fextra.close(); - uploadRemoteProject( mApiExtra, mWorkspaceName, projectName ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); - downloadRemoteProject( serverMirror, mWorkspaceName, projectName ); + uploadRemoteProject( mApiExtra, projectFullName, projectId ); + downloadRemoteProject( mApi, projectFullName, projectId ); + downloadRemoteProject( serverMirror, projectFullName, projectId ); // check that all clients have photos - QStringList photos; - photos << "photoA.jpg" << "photoB.png" << "photoC1-5.png" << "photoC2-3.png" << "photoC2-extra.png"; + const QStringList photos( { "photoA.jpg", "photoB.png", "photoC1-5.png", "photoC2-3.png", "photoC2-extra.png" } ); for ( const QString &photo : photos ) { QFile photo1( photoPathClient1 + "/" + photo ); @@ -2056,22 +2055,22 @@ void TestMerginApi::testSelectiveSyncDisabledInConfig() { "input-selective-sync-dir", "photos" } } ) ); - uploadRemoteProject( serverMirror, mWorkspaceName, projectName ); + uploadRemoteProject( serverMirror, projectFullName, projectId ); QFile f( photoPathClient2 + "/" + "photoC2-should-not-download.png" ); f.open( QIODevice::WriteOnly ); f.close(); - uploadRemoteProject( mApiExtra, mWorkspaceName, projectName ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); - downloadRemoteProject( serverMirror, mWorkspaceName, projectName ); + uploadRemoteProject( mApiExtra, projectFullName, projectId ); + downloadRemoteProject( mApi, projectFullName, projectId ); + downloadRemoteProject( serverMirror, projectFullName, projectId ); // File should be on server mirror and should not be on client 1 - QFile fverify( serverMirrorDataPath + "/" + projectName + "/" + "photos" + "/" + "photoC2-should-not-download.png" ); - QVERIFY( fverify.exists() ); + QFile fVerify( serverMirrorDataPath + "/" + projectName + "/" + "photos" + "/" + "photoC2-should-not-download.png" ); + QVERIFY( fVerify.exists() ); - QFile fverify2( photoPathClient1 + "/" + "photoC2-should-not-download.png" ); - QVERIFY( !fverify2.exists() ); + QFile fVerify2( photoPathClient1 + "/" + "photoC2-should-not-download.png" ); + QVERIFY( !fVerify2.exists() ); delete serverMirror; delete serverMirrorProjects; @@ -2079,36 +2078,38 @@ void TestMerginApi::testSelectiveSyncDisabledInConfig() void TestMerginApi::testSelectiveSyncChangeSyncFolder() { - /* - * Case: Change selective sync folder in mergin-config in an existing project with photos and selective sync enabled. - * - * We will create another API client that will serve as a server mirror, it will not use selective sync, - * but will simulate as if someone manipulated project from browser via Mergin - */ QString serverMirrorDataPath = mApi->projectsPath() + "/" + "serverMirror"; QDir serverMirrorDataDir( serverMirrorDataPath ); if ( !serverMirrorDataDir.exists() ) - serverMirrorDataDir.mkpath( serverMirrorDataPath ); + if ( !serverMirrorDataDir.mkpath( serverMirrorDataPath ) ) + { + qDebug() << QString( "Failed to create directory for %1" ).arg( serverMirrorDataPath ); + }; LocalProjectsManager *serverMirrorProjects = new LocalProjectsManager( serverMirrorDataPath + "/" ); MerginApi *serverMirror = new MerginApi( *serverMirrorProjects, this ); serverMirror->setSupportsSelectiveSync( false ); // Create a project with photos and mergin-config - QString projectName = "testSelectiveSyncChangeSyncFolder"; + const QString projectName = "testSelectiveSyncChangeSyncFolder"; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); - QString projectClient1 = mApi->projectsPath() + "/" + projectName; - QString projectClient2 = mApiExtra->projectsPath() + "/" + projectName; - QString projectServer = serverMirror->projectsPath() + "/" + projectName; + const QString projectClient1 = mApi->projectsPath() + "/" + projectName; + const QString projectClient2 = mApiExtra->projectsPath() + "/" + projectName; + const QString projectServer = serverMirror->projectsPath() + "/" + projectName; createRemoteProject( mApi, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); // Create photo files QDir dir; QString photoPathClient1( projectClient1 + "/" + "photos" ); if ( !dir.exists( photoPathClient1 ) ) - dir.mkpath( photoPathClient1 ); + if ( !dir.mkpath( photoPathClient1 ) ) + { + qDebug() << QString( "Failed to create directory for %1" ).arg( photoPathClient1 ); + }; QFile file( photoPathClient1 + "/" + "photoC1-A.jpg" ); file.open( QIODevice::WriteOnly ); @@ -2118,8 +2119,8 @@ void TestMerginApi::testSelectiveSyncChangeSyncFolder() file1.open( QIODevice::WriteOnly ); file1.close(); - uploadRemoteProject( mApi, mWorkspaceName, projectName ); - downloadRemoteProject( serverMirror, mWorkspaceName, projectName ); + uploadRemoteProject( mApi, projectFullName, projectId ); + downloadRemoteProject( serverMirror, projectFullName, projectId ); QString configFilePath = projectServer + "/" + "mergin-config.json"; QVERIFY( createJsonFile( configFilePath, @@ -2128,11 +2129,11 @@ void TestMerginApi::testSelectiveSyncChangeSyncFolder() { "input-selective-sync-dir", "" } } ) ); - uploadRemoteProject( serverMirror, mWorkspaceName, projectName ); - downloadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + uploadRemoteProject( serverMirror, projectFullName, projectId ); + downloadRemoteProject( mApiExtra, projectFullName, projectId ); // client 2 adds photos to project root, client 1 to photos subfolder - QString photoPathClient2( projectClient2 ); + const QString &photoPathClient2( projectClient2 ); QFile fileExtra( photoPathClient2 + "/" + "photoC1-A.jpg" ); QVERIFY( !fileExtra.exists() ); @@ -2142,7 +2143,10 @@ void TestMerginApi::testSelectiveSyncChangeSyncFolder() QDir photoDirExtra( photoPathClient2 ); if ( !photoDirExtra.exists() ) - photoDirExtra.mkpath( photoPathClient2 ); + if ( !photoDirExtra.mkpath( photoPathClient2 ) ) + { + qDebug() << QString( "Failed to create directory for %1" ).arg( photoPathClient2 ); + }; // simulate some traffic, let both clients create few photos several times (so that project has longer history) for ( int i : { 1, 2, 3, 4, 5 } ) @@ -2155,11 +2159,11 @@ void TestMerginApi::testSelectiveSyncChangeSyncFolder() f2.open( QIODevice::WriteOnly ); f2.close(); - uploadRemoteProject( mApiExtra, mWorkspaceName, projectName ); - uploadRemoteProject( mApi, mWorkspaceName, projectName ); + uploadRemoteProject( mApiExtra, projectFullName, projectId ); + uploadRemoteProject( mApi, projectFullName, projectId ); } - downloadRemoteProject( serverMirror, mWorkspaceName, projectName ); + downloadRemoteProject( serverMirror, projectFullName, projectId ); // Let's change selective sync folder only to photos subfolder InputUtils::removeFile( configFilePath ); @@ -2172,18 +2176,18 @@ void TestMerginApi::testSelectiveSyncChangeSyncFolder() } ) ); // Sync changed config - uploadRemoteProject( serverMirror, mWorkspaceName, projectName ); + uploadRemoteProject( serverMirror, projectFullName, projectId ); // Client 1 should now download all missing files from project root directory - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); QFile fextra( photoPathClient2 + "/" + "photoC2-extra.png" ); fextra.open( QIODevice::WriteOnly ); fextra.close(); - uploadRemoteProject( mApiExtra, mWorkspaceName, projectName ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); - downloadRemoteProject( serverMirror, mWorkspaceName, projectName ); + uploadRemoteProject( mApiExtra, projectFullName, projectId ); + downloadRemoteProject( mApi, projectFullName, projectId ); + downloadRemoteProject( serverMirror, projectFullName, projectId ); /* * Check that: @@ -2226,22 +2230,22 @@ void TestMerginApi::testSelectiveSyncChangeSyncFolder() { "input-selective-sync-dir", "" } } ) ); - uploadRemoteProject( serverMirror, mWorkspaceName, projectName ); + uploadRemoteProject( serverMirror, projectFullName, projectId ); QFile f( photoPathClient2 + "/" + "photoC2-should-not-download.png" ); f.open( QIODevice::WriteOnly ); f.close(); - uploadRemoteProject( mApiExtra, mWorkspaceName, projectName ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); - downloadRemoteProject( serverMirror, mWorkspaceName, projectName ); + uploadRemoteProject( mApiExtra, projectFullName, projectId ); + downloadRemoteProject( mApi, projectFullName, projectId ); + downloadRemoteProject( serverMirror, projectFullName, projectId ); // File should be on server mirror and should not be on client 1 - QFile fverify( serverMirrorProjectPath + "/" + "photoC2-should-not-download.png" ); - QVERIFY( fverify.exists() ); + QFile fVerify( serverMirrorProjectPath + "/" + "photoC2-should-not-download.png" ); + QVERIFY( fVerify.exists() ); - QFile fverify2( projectClient1 + "/" + "photoC2-should-not-download.png" ); - QVERIFY( !fverify2.exists() ); + QFile fVerify2( projectClient1 + "/" + "photoC2-should-not-download.png" ); + QVERIFY( !fVerify2.exists() ); delete serverMirror; delete serverMirrorProjects; @@ -2249,37 +2253,38 @@ void TestMerginApi::testSelectiveSyncChangeSyncFolder() void TestMerginApi::testSelectiveSyncCorruptedFormat() { - - /* - * Case: Test what happens when someone uploads not valid config file (not valid json) - * - * We will create another API client that will serve as a server mirror, it will not use selective sync, - * but will simulate as if someone manipulated project from browser via Mergin - */ - QString serverMirrorDataPath = mApi->projectsPath() + "/" + "serverMirror"; - QDir serverMirrorDataDir( serverMirrorDataPath ); + const QString serverMirrorDataPath = mApi->projectsPath() + "/" + "serverMirror"; + const QDir serverMirrorDataDir( serverMirrorDataPath ); if ( !serverMirrorDataDir.exists() ) - serverMirrorDataDir.mkpath( serverMirrorDataPath ); + if ( !serverMirrorDataDir.mkpath( serverMirrorDataPath ) ) + { + qDebug() << QString( "Failed to create directory for %1" ).arg( serverMirrorDataPath ); + }; LocalProjectsManager *serverMirrorProjects = new LocalProjectsManager( serverMirrorDataPath + "/" ); MerginApi *serverMirror = new MerginApi( *serverMirrorProjects, this ); serverMirror->setSupportsSelectiveSync( false ); // Create a project with photos and mergin-config - QString projectName = "testSelectiveSyncCorruptedFormat"; + const QString projectName = "testSelectiveSyncCorruptedFormat"; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); - QString projectClient1 = mApi->projectsPath() + "/" + projectName; - QString projectClient2 = mApiExtra->projectsPath() + "/" + projectName; - QString projectServer = serverMirror->projectsPath() + "/" + projectName; + const QString projectClient1 = mApi->projectsPath() + "/" + projectName; + const QString projectClient2 = mApiExtra->projectsPath() + "/" + projectName; + const QString projectServer = serverMirror->projectsPath() + "/" + projectName; createRemoteProject( mApi, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); // Create photo files - QDir dir; - QString photoPathClient1( projectClient1 + "/" + "photos" ); + const QDir dir; + const QString photoPathClient1( projectClient1 + "/" + "photos" ); if ( !dir.exists( photoPathClient1 ) ) - dir.mkpath( photoPathClient1 ); + if ( !dir.mkpath( photoPathClient1 ) ) + { + qDebug() << QString( "Failed to create directory for %1" ).arg( photoPathClient1 ); + }; QFile file( photoPathClient1 + "/" + "photoC1-A.jpg" ); file.open( QIODevice::WriteOnly ); @@ -2289,57 +2294,50 @@ void TestMerginApi::testSelectiveSyncCorruptedFormat() file1.open( QIODevice::WriteOnly ); file1.close(); - uploadRemoteProject( mApi, mWorkspaceName, projectName ); - downloadRemoteProject( serverMirror, mWorkspaceName, projectName ); + uploadRemoteProject( mApi, projectFullName, projectId ); + downloadRemoteProject( serverMirror, projectFullName, projectId ); // add corrupted config file - QString configFilePath = projectServer + "/" + "mergin-config.json"; + const QString configFilePath = projectServer + "/" + "mergin-config.json"; QVERIFY( QFile::copy( mTestDataPath + "/mergin-config-corrupted.json", configFilePath ) ); - uploadRemoteProject( serverMirror, mWorkspaceName, projectName ); - downloadRemoteProject( mApiExtra, mWorkspaceName, projectName ); + uploadRemoteProject( serverMirror, projectFullName, projectId ); + downloadRemoteProject( mApiExtra, projectFullName, projectId ); // client 2 should have all photos from client 1 - QString photoPathClient2( projectClient2 + "/" + "photos" ); + const QString photoPathClient2( projectClient2 + "/" + "photos" ); - QFile fileExtra( photoPathClient2 + "/" + "photoC1-A.jpg" ); + const QFile fileExtra( photoPathClient2 + "/" + "photoC1-A.jpg" ); QVERIFY( fileExtra.exists() ); - QFile fileExtra1( photoPathClient2 + "/" + "photoC1-B.png" ); + const QFile fileExtra1( photoPathClient2 + "/" + "photoC1-B.png" ); QVERIFY( fileExtra1.exists() ); } void TestMerginApi::testSynchronizationViaManager() { - // - // 1. instantiate sync manager - // 2. create remote project & download it - // 3. add some data - // 4. sync it via manager - // 5. check if all signals are called - // - - SynchronizationManager syncmanager( mApi ); + SynchronizationManager syncManager( mApi ); - QString projectname( QStringLiteral( "testSynchronizationViaManager" ) ); - QString projectfullname = MerginApi::getFullProjectName( mWorkspaceName, projectname ); + const QString projectName( QStringLiteral( "testSynchronizationViaManager" ) ); + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); - createRemoteProject( mApiExtra, mWorkspaceName, projectname, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); - downloadRemoteProject( mApi, mWorkspaceName, projectname ); + createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); refreshProjectsModel( ProjectsModel::LocalProjectsModel ); - Project project = mLocalProjectsModel->projectFromId( mWorkspaceName + '/' + projectname ); + Project project = mLocalProjectsModel->projectFromId( projectId ); QFile::copy( mTestDataPath + "/" + TEST_PROJECT_NAME + "/test1.txt", project.local.projectDir + "/data.txt" ); - QSignalSpy syncStartedSpy( &syncmanager, &SynchronizationManager::syncStarted ); - QSignalSpy syncFinishedSpy( &syncmanager, &SynchronizationManager::syncFinished ); - QSignalSpy syncProgressedSpy( &syncmanager, &SynchronizationManager::syncProgressChanged ); + QSignalSpy syncStartedSpy( &syncManager, &SynchronizationManager::syncStarted ); + QSignalSpy syncFinishedSpy( &syncManager, &SynchronizationManager::syncFinished ); + QSignalSpy syncProgressedSpy( &syncManager, &SynchronizationManager::syncProgressChanged ); - syncmanager.syncProject( project ); + syncManager.syncProject( project ); - QVERIFY( syncmanager.hasPendingSync( projectfullname ) ); + QVERIFY( syncManager.hasPendingSync( projectId ) ); syncProgressedSpy.wait( TestUtils::SHORT_REPLY ); QVERIFY( syncProgressedSpy.count() ); @@ -2353,28 +2351,21 @@ void TestMerginApi::testSynchronizationViaManager() void TestMerginApi::testAutosync() { - // - // 1. copy test project to temp - // 2. allow autosync controller - // 3. load the project - // 4. make some changes in the project - // 5. make sure autosync controller triggers that data has changed - // + const QString projectName = QStringLiteral( "testAutosync" ); + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); + const QString projectDir = QDir::tempPath() + "/" + projectName; + const QString projectFilename = "quickapp_project.qgs"; - QString projectname = QStringLiteral( "testAutosync" ); - QString projectdir = QDir::tempPath() + "/" + projectname; - QString projectfilename = "quickapp_project.qgs"; - - InputUtils::cpDir( TestUtils::testDataDir() + "/planes", projectdir ); + InputUtils::cpDir( TestUtils::testDataDir() + "/planes", projectDir ); MapThemesModel mtm; AppSettings as; ActiveLayer al; ActiveProject activeProject( as, al, mApi->localProjectsManager() ); - mApi->localProjectsManager().addLocalProject( projectdir, projectname ); + mApi->localProjectsManager().addLocalProject( projectDir, projectName ); as.setAutosyncAllowed( true ); - QVERIFY( activeProject.load( projectdir + "/" + projectfilename ) ); + QVERIFY( activeProject.load( projectDir + "/" + projectFilename ) ); QVERIFY( activeProject.localProject().isValid() ); QSignalSpy syncSpy( &activeProject, &ActiveProject::syncActiveProject ); @@ -2404,17 +2395,6 @@ void TestMerginApi::testAutosync() void TestMerginApi::testAutosyncFailure() { - // - // 1. copy test project to temp - // 2. load it - // 3. create autosync controller - // 4. sign out - // 5. make some changes in the project - // 6. make sure autosync controller has correct failure state - // 7. sign back in - // - - // Will be added with incremental requests } void TestMerginApi::testOfflineCache() @@ -2445,10 +2425,10 @@ void TestMerginApi::testRegisterAndDelete() QSKIP( "testRegisterAndDelete requires USE_MM_SERVER_API_KEY" ); #endif - QString password = mApi->userAuth()->password(); + const QString password = mApi->userAuth()->password(); - QString quiteRandom = CoreUtils::uuidWithoutBraces( QUuid::createUuid() ).right( 15 ).replace( "-", "" ); - QString email = "test_" + quiteRandom + "@nonexistant.email.com"; + const QString quiteRandom = CoreUtils::uuidWithoutBraces( QUuid::createUuid() ).right( 15 ).replace( "-", "" ); + const QString email = "test_" + quiteRandom + "@nonexistent.email.com"; qDebug() << "email:" << email; // do not want to be authorized @@ -2482,9 +2462,9 @@ void TestMerginApi::testCreateWorkspace() QSKIP( "testCreateWorkspace requires USE_MM_SERVER_API_KEY" ); #endif // we need to register new user for tests and assign its credentials to env vars - QString username = TestUtils::generateUsername(); - QString password = TestUtils::generatePassword(); - QString email = TestUtils::generateEmail(); + const QString username = TestUtils::generateUsername(); + const QString password = TestUtils::generatePassword(); + const QString email = TestUtils::generateEmail(); qDebug() << "REGISTERING NEW TEST USER WITH EMAIL:" << email; @@ -2531,9 +2511,9 @@ void TestMerginApi::testExcludeFromSync() deleteLocalDir( mApi, "testExcludeFromSync" ); // Set selective sync directory - QString selectiveSyncDir( mApi->projectsPath() + "/testExcludeFromSync" ); + const QString selectiveSyncDir( mApi->projectsPath() + "/testExcludeFromSync" ); - QList testFiles = + QList testFiles = { selectiveSyncDir + "/data.gpkg", selectiveSyncDir + "/image.png", @@ -2576,7 +2556,7 @@ void TestMerginApi::testExcludeFromSync() //////// HELPER FUNCTIONS //////// -MerginProjectsList TestMerginApi::getProjectList( QString tag ) +MerginProjectsList TestMerginApi::getProjectList( const QString &tag ) { QSignalSpy spy( mApi, &MerginApi::listProjectsFinished ); mApi->listProjects( QString(), tag ); @@ -2591,10 +2571,10 @@ MerginProjectsList TestMerginApi::projectListFromSpy( QSignalSpy &spy ) if ( !spy.isEmpty() ) { - QList response = spy.takeFirst(); + const QList response = spy.takeFirst(); - // get projects emited from MerginAPI, it is first argument in listProjectsFinished signal - if ( response.length() > 0 ) + // get projects emitted from MerginAPI, it is first argument in listProjectsFinished signal + if ( !response.isEmpty() ) projects = qvariant_cast( response.at( 0 ) ); } return projects; @@ -2606,16 +2586,16 @@ int TestMerginApi::serverVersionFromSpy( QSignalSpy &spy ) if ( !spy.isEmpty() ) { - QList response = spy.takeFirst(); + const QList response = spy.takeFirst(); - // get version number emited from MerginApi::syncProjectFinished, it is third argument + // get version number emitted from MerginApi::syncProjectFinished, it is third argument if ( response.length() >= 4 ) serverVersion = response.at( 3 ).toInt(); } return serverVersion; } -void TestMerginApi::createRemoteProject( MerginApi *api, const QString &projectNamespace, const QString &projectName, const QString &sourcePath, bool force ) +void TestMerginApi::createRemoteProject( MerginApi *api, const QString &projectNamespace, const QString &projectName, const QString &sourcePath, const bool force ) { if ( force ) { @@ -2624,35 +2604,36 @@ void TestMerginApi::createRemoteProject( MerginApi *api, const QString &projectN // create a project QSignalSpy spy( api, &MerginApi::projectCreated ); - api->createProject( projectNamespace, projectName, true ); + api->createProject( projectNamespace, projectName, CoreUtils::uuidWithoutBraces( QUuid::createUuid() ), true ); QVERIFY( spy.wait( TestUtils::SHORT_REPLY ) ); QCOMPARE( spy.count(), 1 ); QCOMPARE( spy.takeFirst().at( 1 ).toBool(), true ); // Copy data - QString projectDir = api->projectsPath() + "/" + projectName + "/"; + const QString projectDir = api->projectsPath() + "/" + projectName + "/"; InputUtils::cpDir( sourcePath, projectDir ); // make MerginApi aware of the project and its directory - api->localProjectsManager().addMerginProject( projectDir, projectNamespace, projectName ); + const QString merginProjectId = projectIdFromProjectFullName( api, projectNamespace, projectName ); + api->localProjectsManager().addMerginProject( projectDir, projectNamespace, projectName, merginProjectId ); // Upload data QSignalSpy spy3( api, &MerginApi::syncProjectFinished ); - api->pushProject( projectNamespace, projectName ); + api->pushProject( CoreUtils::getFullProjectName( projectNamespace, projectName ), merginProjectId ); QVERIFY( spy3.wait( TestUtils::LONG_REPLY ) ); QCOMPARE( spy3.count(), 1 ); - QList arguments = spy3.takeFirst(); - int version = arguments.at( 2 ).toInt(); + const QList arguments = spy3.takeFirst(); + const int version = arguments.at( 2 ).toInt(); QCOMPARE( version, 1 ); // Remove the whole project QDir( projectDir ).removeRecursively(); - QFileInfo info( projectDir ); - QDir dir( projectDir ); + const QFileInfo info( projectDir ); + const QDir dir( projectDir ); QCOMPARE( info.size(), 0 ); QVERIFY( dir.isEmpty() ); - api->localProjectsManager().removeLocalProject( MerginApi::getFullProjectName( projectNamespace, projectName ) ); + api->localProjectsManager().removeLocalProject( merginProjectId ); QCOMPARE( QFileInfo( projectDir ).size(), 0 ); QVERIFY( QDir( projectDir ).isEmpty() ); @@ -2666,17 +2647,17 @@ QString TestMerginApi::projectIdFromProjectFullName( MerginApi *api, const QStri return ret; } - QString projectFullName = api->getFullProjectName( projectNamespace, projectName ); + const QString projectFullName = CoreUtils::getFullProjectName( projectNamespace, projectName ); - QNetworkReply *r = api->getProjectInfo( projectFullName ); + QNetworkReply *r = api->getProjectInfo( projectFullName, QString() ); Q_ASSERT( r ); QSignalSpy spy( r, &QNetworkReply::finished ); spy.wait( TestUtils::SHORT_REPLY ); if ( r->error() == QNetworkReply::NoError ) { - QByteArray data = r->readAll(); - MerginProjectMetadata serverProject = MerginProjectMetadata::fromJson( data ); + const QByteArray data = r->readAll(); + const MerginProjectMetadata serverProject = MerginProjectMetadata::fromJson( data ); ret = serverProject.projectId; } else @@ -2695,15 +2676,15 @@ void TestMerginApi::deleteRemoteProjectNow( MerginApi *api, const QString &proje return; } - QString projectId = projectIdFromProjectFullName( api, projectNamespace, projectName ); + const QString projectId = projectIdFromProjectFullName( api, projectNamespace, projectName ); if ( projectId.isEmpty() ) { - // probably no such project exist on server + // probably no such project exists on server return; } QNetworkRequest request = api->getDefaultRequest(); - QUrl url( api->mApiRoot + QStringLiteral( "/v2/projects/%1" ).arg( projectId ) ); + const QUrl url( api->mApiRoot + QStringLiteral( "/v2/projects/%1" ).arg( projectId ) ); request.setUrl( url ); qDebug() << "Trying to delete project " << projectName << ", id: " << projectId << " (" << url << ")"; QNetworkReply *r = api->mManager->deleteResource( request ); @@ -2715,45 +2696,45 @@ void TestMerginApi::deleteRemoteProjectNow( MerginApi *api, const QString &proje } -void TestMerginApi::deleteLocalProject( MerginApi *api, const QString &projectNamespace, const QString &projectName ) +void TestMerginApi::deleteLocalProject( const MerginApi *api, const QString &projectId ) { - LocalProject project = api->getLocalProject( MerginApi::getFullProjectName( projectNamespace, projectName ) ); + const LocalProject project = api->getLocalProject( projectId ); QVERIFY( project.isValid() ); QVERIFY( project.projectDir.startsWith( api->projectsPath() ) ); // just to make sure we don't delete something wrong (-: api->localProjectsManager().removeLocalProject( project.id() ); } -void TestMerginApi::deleteLocalDir( MerginApi *api, const QString &dirPath ) +void TestMerginApi::deleteLocalDir( const MerginApi *api, const QString &dirPath ) { QDir dir( api->projectsPath() + "/" + dirPath ); QVERIFY( dir.removeRecursively() ); } -void TestMerginApi::downloadRemoteProject( MerginApi *api, const QString &projectNamespace, const QString &projectName ) +void TestMerginApi::downloadRemoteProject( MerginApi *api, const QString &projectFullName, const QString &projectId ) { int serverVersion; - downloadRemoteProject( api, projectNamespace, projectName, serverVersion ); + downloadRemoteProject( api, projectFullName, projectId, serverVersion ); } -void TestMerginApi::downloadRemoteProject( MerginApi *api, const QString &projectNamespace, const QString &projectName, int &serverVersion ) +void TestMerginApi::downloadRemoteProject( MerginApi *api, const QString &projectFullName, const QString &projectId, int &serverVersion ) { QSignalSpy spy( api, &MerginApi::syncProjectFinished ); - api->pullProject( projectNamespace, projectName ); + api->pullProject( projectFullName, projectId ); QCOMPARE( api->transactions().count(), 1 ); QVERIFY( spy.wait( TestUtils::LONG_REPLY * 5 ) ); serverVersion = serverVersionFromSpy( spy ); } -void TestMerginApi::uploadRemoteProject( MerginApi *api, const QString &projectNamespace, const QString &projectName ) +void TestMerginApi::uploadRemoteProject( MerginApi *api, const QString &projectFullName, const QString &projectId ) { int serverVersion; - uploadRemoteProject( api, projectNamespace, projectName, serverVersion ); + uploadRemoteProject( api, projectFullName, projectId, serverVersion ); } -void TestMerginApi::uploadRemoteProject( MerginApi *api, const QString &projectNamespace, const QString &projectName, int &serverVersion ) +void TestMerginApi::uploadRemoteProject( MerginApi *api, const QString &projectFullName, const QString &projectId, int &serverVersion ) { - api->pushProject( projectNamespace, projectName ); + api->pushProject( projectFullName, projectId ); QSignalSpy spy( api, &MerginApi::syncProjectFinished ); QVERIFY( spy.wait( TestUtils::LONG_REPLY * 30 ) ); QCOMPARE( spy.count(), 1 ); @@ -2763,7 +2744,7 @@ void TestMerginApi::uploadRemoteProject( MerginApi *api, const QString &projectN void TestMerginApi::writeFileContent( const QString &filename, const QByteArray &data ) { QFile f( filename ); - bool ok = f.open( QIODeviceBase::WriteOnly ); + const bool ok = f.open( QIODeviceBase::WriteOnly ); Q_ASSERT( ok ); f.write( data ); f.flush(); @@ -2778,30 +2759,33 @@ QByteArray TestMerginApi::readFileContent( const QString &filename ) qDebug() << "Filename " << filename << " does not exist"; Q_ASSERT( false ); } - bool ok = f.open( QIODeviceBase::ReadOnly ); + const bool ok = f.open( QIODeviceBase::ReadOnly ); Q_ASSERT( ok ); QByteArray data = f.readAll(); f.close(); return data; } -void TestMerginApi::createLocalProject( const QString projectDir ) +void TestMerginApi::createLocalProject( const QString &projectDir ) { - QDir().mkdir( projectDir ); - bool r0 = QFile::copy( mTestDataPath + "/diff_project/base.gpkg", projectDir + "/base.gpkg" ); + if ( !QDir().mkdir( projectDir ) ) + { + qDebug() << QString( "Failed to create directory for %1" ).arg( projectDir ); + }; + const bool r0 = QFile::copy( mTestDataPath + "/diff_project/base.gpkg", projectDir + "/base.gpkg" ); QVERIFY( r0 ); } bool TestMerginApi::createJsonFile( const QString &path, const QVariantMap ¶ms ) { - QJsonObject json = QJsonObject::fromVariantMap( params ); - QJsonDocument doc( json ); - QByteArray data = doc.toJson(); + const QJsonObject json = QJsonObject::fromVariantMap( params ); + const QJsonDocument doc( json ); + const QByteArray data = doc.toJson(); writeFileContent( path, data ); - QFile config( path ); + const QFile config( path ); return config.exists(); } @@ -2861,12 +2845,12 @@ void TestMerginApi::testServerUpgrade() QCOMPARE( mApi->serverType(), MerginServerType::SAAS ); } -void TestMerginApi::testServerError() +void TestMerginApi::testServerError() const { - QString msg = mApi->extractServerErrorMsg( "{\"detail\": \"Some error occured.\"}" ); - QCOMPARE( msg, QStringLiteral( "Some error occured." ) ); + QString msg = mApi->extractServerErrorMsg( "{\"detail\": \"Some error occurred.\"}" ); + QCOMPARE( msg, QStringLiteral( "Some error occurred." ) ); - msg = mApi->extractServerErrorMsg( "{\"name\": \"Some error occured.\"}" ); + msg = mApi->extractServerErrorMsg( "{\"name\": \"Some error occurred.\"}" ); QCOMPARE( msg, QStringLiteral( "[can't parse server error]" ) ); msg = mApi->extractServerErrorMsg( "{\"name\": [\"Field must be between 4 and 25 characters long.\"]}" ); @@ -2900,7 +2884,7 @@ void TestMerginApi::testRegistration() QCOMPARE( spy.takeFirst().at( 1 ).toInt(), RegistrationError::RegistrationErrorType::TOC ); } -void TestMerginApi::testParseVersion() +void TestMerginApi::testParseVersion() const { int major, minor; @@ -2951,34 +2935,34 @@ void TestMerginApi::testParseVersion() void TestMerginApi::testUpdateProjectMetadataRole() { - QString projectName = "testUpdateProjectMetadataRole"; + const QString projectName = "testUpdateProjectMetadataRole"; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); - downloadRemoteProject( mApi, mWorkspaceName, projectName ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); + downloadRemoteProject( mApi, projectFullName, projectId ); - LocalProject projectInfo = mApi->localProjectsManager().projectFromMerginName( mWorkspaceName, projectName ); + const LocalProject projectInfo = mApi->localProjectsManager().projectFromProjectId( projectId ); QVERIFY( projectInfo.isValid() ); - QString fullProjectName = MerginApi::getFullProjectName( mWorkspaceName, projectName ); - // Test 1: Initial role should be 'owner' - QString cachedRole = mApi->getCachedProjectRole( fullProjectName ); + QString cachedRole = mApi->getCachedProjectRole( projectId ); QCOMPARE( cachedRole, QString( "owner" ) ); // Test 2: Update cached role to 'reader' - QString newRole = "reader"; - bool updateSuccess = mApi->updateCachedProjectRole( fullProjectName, newRole ); + const QString newRole = "reader"; + const bool updateSuccess = mApi->updateCachedProjectRole( projectId, newRole ); QVERIFY( updateSuccess ); // Verify role was updated in cache - cachedRole = mApi->getCachedProjectRole( fullProjectName ); + cachedRole = mApi->getCachedProjectRole( projectId ); QCOMPARE( cachedRole, QString( "reader" ) ); // Role in server wasn't updated and stills "owner" => let's reload it from server and see if it updates in cached QSignalSpy spy( mApi, &MerginApi::projectRoleUpdated ); - mApi->reloadProjectRole( fullProjectName ); + mApi->reloadProjectRole( projectId ); QVERIFY( spy.wait() ); - cachedRole = mApi->getCachedProjectRole( fullProjectName ); + cachedRole = mApi->getCachedProjectRole( projectId ); QCOMPARE( cachedRole, QString( "owner" ) ); // Clean up @@ -2990,17 +2974,19 @@ void TestMerginApi::testDownloadWithNetworkError() // Store original manager QNetworkAccessManager *originalManager = mApi->networkManager(); - QString projectName = "testDownloadRetry"; + const QString projectName = "testDownloadRetry"; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); // Errors to test - QList errorsToTest = + QList errorsToTest = { QNetworkReply::TimeoutError, QNetworkReply::NetworkSessionFailedError }; - foreach ( QNetworkReply::NetworkError networkError, errorsToTest ) + for ( QNetworkReply::NetworkError networkError : errorsToTest ) { // Create mock manager - initially not failing MockNetworkManager *failingManager = new MockNetworkManager( this ); @@ -3012,12 +2998,12 @@ void TestMerginApi::testDownloadWithNetworkError() QSignalSpy finishSpy( mApi, &MerginApi::syncProjectFinished ); // Trigger the current network error when download starts - connect( mApi, &MerginApi::pullFilesStarted, this, [this, failingManager, networkError]() + connect( mApi, &MerginApi::pullFilesStarted, this, [this, failingManager, networkError] { failingManager->setShouldFail( true, networkError ); } ); - mApi->pullProject( mWorkspaceName, projectName ); + mApi->pullProject( projectFullName, projectId ); // Verify a transaction was created QCOMPARE( mApi->transactions().count(), 1 ); @@ -3040,7 +3026,7 @@ void TestMerginApi::testDownloadWithNetworkError() QVERIFY( !arguments.at( 1 ).toBool() ); // Verify no local project was created - LocalProject localProject = mApi->localProjectsManager().projectFromMerginName( mWorkspaceName, projectName ); + LocalProject localProject = mApi->localProjectsManager().projectFromProjectId( projectId ); QVERIFY( !localProject.isValid() ); // Disconnect all signals @@ -3057,9 +3043,12 @@ void TestMerginApi::testDownloadWithNetworkErrorRecovery() // Store original manager QNetworkAccessManager *originalManager = mApi->networkManager(); - QString projectName = "testDownloadRetryRecovery"; + const QString projectName = "testDownloadRetryRecovery"; + const QString projectFullName = CoreUtils::getFullProjectName( mWorkspaceName, projectName ); createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); + const QString projectId = projectIdFromProjectFullName( mApi, mWorkspaceName, projectName ); + // Create mock manager - initially not failing MockNetworkManager *failingManager = new MockNetworkManager( this ); mApi->setNetworkManager( failingManager ); @@ -3074,7 +3063,7 @@ void TestMerginApi::testDownloadWithNetworkErrorRecovery() QNetworkReply::NetworkError networkError = QNetworkReply::TimeoutError; // Reset network after two retries - connect( mApi, &MerginApi::downloadItemRetried, this, [&retryCount, failingManager, this]() + connect( mApi, &MerginApi::downloadItemRetried, this, [&retryCount, failingManager, this] { retryCount++; if ( retryCount == 2 ) @@ -3086,12 +3075,12 @@ void TestMerginApi::testDownloadWithNetworkErrorRecovery() } ); // Trigger network error when download starts - connect( mApi, &MerginApi::pullFilesStarted, this, [failingManager, networkError]() + connect( mApi, &MerginApi::pullFilesStarted, this, [failingManager, networkError] { failingManager->setShouldFail( true, networkError ); } ); - mApi->pullProject( mWorkspaceName, projectName ); + mApi->pullProject( projectFullName, projectId ); // Verify a transaction was created QCOMPARE( mApi->transactions().count(), 1 ); @@ -3110,7 +3099,7 @@ void TestMerginApi::testDownloadWithNetworkErrorRecovery() QVERIFY( arguments.at( 1 ).toBool() ); // Verify local project was created successfully - LocalProject localProject = mApi->localProjectsManager().projectFromMerginName( mWorkspaceName, projectName ); + LocalProject localProject = mApi->localProjectsManager().projectFromProjectId( projectId ); QVERIFY( localProject.isValid() ); // Verify project files were downloaded correctly @@ -3126,19 +3115,16 @@ void TestMerginApi::testDownloadWithNetworkErrorRecovery() void TestMerginApi::testMerginConfigFromFile() { - QString tempFilePath; - MerginConfig config; - // 1 => valid JSON - tempFilePath = QDir::tempPath() + "/test_valid_config.json"; + QString tempFilePath = QDir::tempPath() + "/test_valid_config.json"; { QFile file( tempFilePath ); QVERIFY( file.open( QIODevice::WriteOnly ) ); - QByteArray data = "{\"input-selective-sync\": true, \"input-selective-sync-dir\": \"photos\"}"; + const QByteArray data = "{\"input-selective-sync\": true, \"input-selective-sync-dir\": \"photos\"}"; file.write( data ); file.close(); } - config = MerginConfig::fromFile( tempFilePath ); + MerginConfig config = MerginConfig::fromFile( tempFilePath ); QVERIFY( config.isValid ); QCOMPARE( config.selectiveSyncEnabled, true ); QCOMPARE( config.selectiveSyncDir, QString( "photos" ) ); @@ -3149,7 +3135,7 @@ void TestMerginApi::testMerginConfigFromFile() { QFile file( tempFilePath ); QVERIFY( file.open( QIODevice::WriteOnly ) ); - QByteArray data = "this is not valid JSON"; + const QByteArray data = "this is not valid JSON"; file.write( data ); file.close(); } @@ -3219,7 +3205,7 @@ void TestMerginApi::testHasLocalChangesWithSelectiveSyncEnabled() QList localFilesNoChange; localFilesNoChange.append( serverIncluded ); - bool result = mApi->hasLocalChanges( oldServerFiles, localFilesNoChange, projectDir, config ); + bool result = MerginApi::hasLocalChanges( oldServerFiles, localFilesNoChange, projectDir, config ); QVERIFY( !result ); // second scenario => local file list contains a modified version of non‑excluded file @@ -3231,14 +3217,14 @@ void TestMerginApi::testHasLocalChangesWithSelectiveSyncEnabled() localFilesChanged.append( modifiedIncluded ); } - result = mApi->hasLocalChanges( oldServerFiles, localFilesChanged, projectDir, config ); + result = MerginApi::hasLocalChanges( oldServerFiles, localFilesChanged, projectDir, config ); QVERIFY( result ); } void TestMerginApi::testHasLocalProjectChanges() { // temporary project directory - QString projectName = "testHasLocalProjectChanges"; + const QString projectName = "testHasLocalProjectChanges"; QTemporaryDir tempDir; QVERIFY( tempDir.isValid() ); QString projectDir = tempDir.path(); diff --git a/app/test/testmerginapi.h b/app/test/testmerginapi.h index ec05207c5..93add3f47 100644 --- a/app/test/testmerginapi.h +++ b/app/test/testmerginapi.h @@ -13,19 +13,16 @@ #include #include -#include "inputconfig.h" -#include -#include -#include +#include "qgsvectorlayer.h" +#include "merginapi.h" +#include "projectsmodel.h" #include "project.h" -#include - class MockReply : public QNetworkReply { public: - explicit MockReply( const QNetworkRequest &request, QNetworkAccessManager::Operation operation, - QObject *parent = nullptr, QNetworkReply::NetworkError errorCode = QNetworkReply::NoError ) + explicit MockReply( const QNetworkRequest &request, const QNetworkAccessManager::Operation operation, + QObject *parent = nullptr, const QNetworkReply::NetworkError errorCode = QNetworkReply::NoError ) : QNetworkReply( parent ) { setRequest( request ); @@ -39,12 +36,12 @@ class MockReply : public QNetworkReply } QMetaObject::invokeMethod( this, "finished", Qt::QueuedConnection ); - open( QIODevice::ReadOnly ); + QIODevice::open( QIODevice::ReadOnly ); } void abort() override {} - qint64 readData( char *data, qint64 maxlen ) override + qint64 readData( char *data, const qint64 maxlen ) override { Q_UNUSED( data ); Q_UNUSED( maxlen ); @@ -66,14 +63,14 @@ class MockNetworkManager : public QNetworkAccessManager , mErrorCode( QNetworkReply::NoError ) {} - void setShouldFail( bool shouldFail, QNetworkReply::NetworkError errorCode = QNetworkReply::NoError ) + void setShouldFail( const bool shouldFail, const QNetworkReply::NetworkError errorCode = QNetworkReply::NoError ) { mShouldFail = shouldFail; mErrorCode = errorCode; } protected: - QNetworkReply *createRequest( Operation op, const QNetworkRequest &request, QIODevice *outgoingData = nullptr ) override + QNetworkReply *createRequest( const Operation op, const QNetworkRequest &request, QIODevice *outgoingData = nullptr ) override { if ( mShouldFail ) { @@ -93,7 +90,7 @@ class TestMerginApi: public QObject Q_OBJECT public: explicit TestMerginApi( MerginApi *api ); - ~TestMerginApi(); + ~TestMerginApi() override; static const QString TEST_PROJECT_NAME; static const QString TEST_EMPTY_FILE_NAME; @@ -104,63 +101,385 @@ class TestMerginApi: public QObject void initTestCase(); void cleanupTestCase(); + /** + * Test first deletes the test project on server if it exists and the creates a new empty project. + */ void testListProject(); + + /** + * Test creates new project on server and then fetches it by \code listProjectsByName \endcode API. + */ void testListProjectsByName(); + + /** + * Test creates a project on server, downloads the project from a scratch using \code pullProject \endcode API + * and checks the integrity of local project. + */ void testDownloadProject(); + + /** + * Test creates new project on server, defines network errors and mocks them via \a MockNetworkManager. + */ void testDownloadWithNetworkError(); + + /** + * Test creates new project on server and tries to download it. Two network errors are mocked from, which + * should the process recover. + */ void testDownloadWithNetworkErrorRecovery(); + + /** + * Test creates new project on server and tries to upload and download files with special characters in name. + */ void testDownloadProjectSpecChars(); + + /** + * Test creates new project on server and cancels download before API is called and then after API is called. + */ void testCancelDownloadProject(); + + /** + * Test creates new project on server and then tries to create the same one again. + */ void testCreateProjectTwice(); + + /** + * Test tries to delete non-existent project from server. + */ void testDeleteNonExistingProject(); + + /** + * Test creates new project on server and deletes it. + */ void testCreateDeleteProject(); + + /** + * Test creates new project on server and tries to upload data to server. First it gets canceled right away, + * secondly it gets canceled when data starts to get uploaded, thirdly it gets uploaded. + */ void testUploadProject(); + + /** + * Test creates new project on server. Tries to upload a file that needs to be split into multiple chunks + * and then also downloads it correctly again in a clean new download. + */ void testMultiChunkUploadDownload(); + + /** + * Test creates new project on server. Tries to upload and download empty file. + */ void testEmptyFileUploadDownload(); + + /** + * Test creates new project on server. Downloads it, adds a file, uploads it, deletes local copy + * and downloads it again. + */ void testPushAddedFile(); + + /** + * Test creates new project on server. Downloads it, removes a file, uploads it, deletes local copy + * and downloads it again. + */ void testPushRemovedFile(); + + /** + * Test creates new project on server. Downloads it, modifies a file, uploads it, deletes local copy + * and downloads it again. + */ void testPushModifiedFile(); + + /** + * Test creates new project on server. Downloads it, uploads it back without changes. + */ void testPushNoChanges(); + + /** + * Test creates new project on server. Downloads it, new file is created on server, checks the server version, + * downloads the newer version. + */ void testUpdateAddedFile(); + + /** + * Test creates new project on server. Downloads it, file is removed on server, checks the server version, + * downloads the newer version. + */ void testUpdateRemovedFiles(); + + /** + * Test creates new project on server. Downloads it, file is removed on server, but also modified locally, + * checks the server version, downloads the newer version. In the end we keep the modified version. + */ void testUpdateRemovedVsModifiedFiles(); + + /** + * This test downloads a project, makes a local update of a file, in the meanwhile it does remote update of + * the same file to create a conflict. Finally, it tries to upload the local change to test the code + * responsible for conflict resolution (renames the local file). + */ void testConflictRemoteUpdateLocalUpdate(); + + /** + * This test downloads a project, creates a new file in the meanwhile it creates the same file on + * the server to create a conflict. Finally, it tries to upload the local change to test the code + * responsible for conflict resolution (renames the local file). + */ void testConflictRemoteAddLocalAdd(); + + /** + * This test simulates creation of edit conflict when two clients are trying to update the same attribute. + * Edit conflict file should be created inside project folder and synced to server + */ void testEditConflictScenario(); + + /** + * This test triggers the situation when the request to upload a project first needs to do an update and + * only afterwards it uploads changes. + */ void testUploadWithUpdate(); + + /** + * This test creates a new project on server, downloads it, afterwards makes changes to gpkg and uploads it. + */ void testDiffUpload(); + + /** + * This test creates a new project on server, downloads it, afterwards makes changes to gpkg in nested + * subdirectory and uploads it. + */ void testDiffSubdirsUpload(); + + /** + * This test creates a new project on server, downloads it. Another device downloads it too and uploads modified + * version of gpkg. Newer version is downloaded again. + */ void testDiffUpdateBasic(); + + /** + * This test creates a new project on server, downloads it. Another device downloads it too and uploads modified + * version of gpkg. Newer version is downloaded again and local changes are rebased on top of the server's change. + */ void testDiffUpdateWithRebase(); + + /** + * This test creates a new project on server, downloads it. Another device downloads it too and uploads modified + * version of gpkg. Newer version is downloaded again and local changes are rebased on top of the server's change. + * \note Test case where the local change is something that geodiff does not support and thus cannot rebase + * the changes (should create a conflict file instead). + */ void testDiffUpdateWithRebaseFailed(); + + /** + * Test case where we download initial version (v1), then there will be two versions with diffs (v2 and v3), + * afterwards we try to update the local project. + */ void testUpdateWithDiffs(); + + /** + * When updating from v3 to v4, it is expected that we will get references to diffs for v2-v3 and v3-v4. + * There was a bug where we always ignored the first one. But it could happen that there is no update in v2-v3, + * and we ended up ignoring v3-v4, ending up with broken base files. + * + * 1. [extra] create project, upload .gpkg (v1) + * 2. [extra] upload a new file (v2) + * 3. [main] download project (v2) + * 4. [extra] upload updated .gpkg (v3) + * 5. [main] update from v2 to v3 + */ void testUpdateWithMissedVersion(); + + /** + * Test creates a local project and uploads it to the server. + */ void testMigrateProject(); + + /** + * When a new project is migrated to Mergin, creating base files for diffable files was omitted. Therefore, + * sync was not properly working resulting into having a conflict file. Test covers creating a new project, + * migrating it to Mergin and both sides sync. + * + * 1. [main] create project with .gpkg (v1) file + * 2. [main] migrate the project to mergin + * 3. [extra] download the project and make changes to .gpkg (v2) + * 4. [main] sync the project (v2), should be valid without conflicts + * 5. [main] make changes to .gpkg (v3) and sync + * 6. [extra] sync the project (v3), should be valid without conflicts + */ void testMigrateProjectAndSync(); + + /** + * Test creates local project, uploads it to server and then removes from server, but keeps the local part. + */ void testMigrateDetachProject(); + + /** + * Case: Clients have the following configuration: selective sync on, selective-sync-dir empty + * (project dir by default) + * + * Action 1: Client 1 uploads some images and Client 2 sync without downloading the images + * + * Action 2: Client 2 uploads an image and do not remove not-synced images. Client 1 syncs without downloading + * the image, still having own images. + */ void testSelectiveSync(); + + /** + * Case: Downloading project with config. + * + * We have the following scenario: + * { + * "input-selective-sync": true, + * "input-selective-sync-dir": "photos" // having subfolder + * } + * + * Action 1: Client 1 creates project with mergin-config and uploads some images, + * Client 2 should sync without downloading the images. + * + * Action 2: Client 2 uploads two images, one in "photos" subdirectory and second in project root. + * Client 1 should sync without downloading the image in "photos" subdirectory and should still have + * own images (they should not be deleted even though Client 2 did not have them when syncing) + */ void testSelectiveSyncSubfolder(); + + /** + * Case: Have a project with photos without mergin config, add it when both clients are using project already to simulate + * users that add mergin config to existing projects. + * + * Procedure: Create project with photos, sync it to both clients, then let Client 1 add mergin config together with several + * pictures and see if the new pictures are NOT synced. + */ void testSelectiveSyncAddConfigToExistingProject(); + + /** + * Case: Remove mergin-config from an existing project with photos. + * + * We will create another API client that will serve as a server mirror, it will not use selective sync, + * but will simulate as if someone manipulated project from browser via Mergin + */ void testSelectiveSyncRemoveConfig(); + + /** + * Case: Disable selective sync in mergin-config in an existing project with photos and selective sync previously enabled. + * + * We will create another API client that will serve as a server mirror, it will not use selective sync, + * but will simulate as if someone manipulated project from browser via Mergin + */ void testSelectiveSyncDisabledInConfig(); + + /** + * Case: Change selective sync folder in mergin-config in an existing project with photos and selective sync enabled. + * + * We will create another API client that will serve as a server mirror, it will not use selective sync, + * but will simulate as if someone manipulated project from browser via Mergin + */ void testSelectiveSyncChangeSyncFolder(); + + /** + * Case: Test what happens when someone uploads not valid config file (not valid json) + * + * We will create another API client that will serve as a server mirror, it will not use selective sync, + * but will simulate as if someone manipulated project from browser via Mergin + */ void testSelectiveSyncCorruptedFormat(); + + /** + * 1. instantiate sync manager + * 2. create remote project & download it + * 3. add some data + * 4. sync it via manager + * 5. check if all signals are called + */ void testSynchronizationViaManager(); + + /** + * 1. copy test project to temp + * 2. allow autosync controller + * 3. load the project + * 4. make some changes in the project + * 5. make sure autosync controller triggers that data has changed + */ void testAutosync(); + + /** + * 1. copy test project to temp + * 2. load it + * 3. create autosync controller + * 4. sign out + * 5. make some changes in the project + * 6. make sure autosync controller has correct failure state + * 7. sign back in + * \todo Will be added with incremental requests + */ void testAutosyncFailure(); + + /** + * Test creates new project on server and downloads it. Then project role is changed locally and again + * fetched from server. + */ void testUpdateProjectMetadataRole(); + + /** + * Test tries to load \a MerginConfig from file. + */ void testMerginConfigFromFile(); + + /** + * Test creates \a MerginConfig with selective sync enabled. Two variants are tested, files included in sync and + * files not included in sync. + */ void testHasLocalChangesWithSelectiveSyncEnabled(); + + /** + * Test multiple scenarios to register local changes. + * 1. Empty metadata and no local files, selective sync not supported + * 2. Metadata has files and no local files, selective sync not supported + * 3. Metadata files equals local files, selective sync supported + * 4. Local files differs from metadata, selective sync supported + */ void testHasLocalProjectChanges(); + void testOfflineCache(); + + /** + * Test creates new user and deletes it. + */ void testRegisterAndDelete(); + + /** + * Test creates new user, new workspace and tries to delete the user (expects to fail). + */ void testCreateWorkspace(); + + // mergin functions + + /** + * Test creates \a MerginConfig and checks if Mergin API parses it correctly. + */ void testExcludeFromSync(); + + /** + * Test querying server config. + */ void testServerType(); + + /** + * Test setting server type. + */ void testServerUpgrade(); - void testServerError(); + + /** + * Test parsing server errors. + */ + void testServerError() const; + + /** + * Test registration checks. + */ void testRegistration(); - void testParseVersion(); + + /** + * Test parsing version string. + */ + void testParseVersion() const; void testApiRoot(); private: @@ -176,7 +495,7 @@ class TestMerginApi: public QObject MerginApi *mApiExtra = nullptr; LocalProjectsManager *mLocalProjectsExtra = nullptr; - MerginProjectsList getProjectList( QString tag = "created" ); + MerginProjectsList getProjectList( const QString &tag = "created" ); MerginProjectsList projectListFromSpy( QSignalSpy &spy ); int serverVersionFromSpy( QSignalSpy &spy ); @@ -194,23 +513,23 @@ class TestMerginApi: public QObject /** * Immediately deletes a project on the server - * If project does not exists, it does nothing. + * If project does not exist, it does nothing. */ void deleteRemoteProjectNow( MerginApi *api, const QString &projectNamespace, const QString &projectName ); //! Downloads a remote project to the local drive, extended version also sets server version - void downloadRemoteProject( MerginApi *api, const QString &projectNamespace, const QString &projectName, int &serverVersion ); - void downloadRemoteProject( MerginApi *api, const QString &projectNamespace, const QString &projectName ); + void downloadRemoteProject( MerginApi *api, const QString &projectFullName, const QString &projectId, int &serverVersion ); + void downloadRemoteProject( MerginApi *api, const QString &projectFullName, const QString &projectId ); //! Uploads any local changes in the local project to the remote project, extended version also sets server version - void uploadRemoteProject( MerginApi *api, const QString &projectNamespace, const QString &projectName, int &serverVersion ); - void uploadRemoteProject( MerginApi *api, const QString &projectNamespace, const QString &projectName ); + void uploadRemoteProject( MerginApi *api, const QString &projectFullName, const QString &projectId, int &serverVersion ); + void uploadRemoteProject( MerginApi *api, const QString &projectFullName, const QString &projectId ); //! Deletes a project from the local drive - void deleteLocalProject( MerginApi *api, const QString &projectNamespace, const QString &projectName ); + void deleteLocalProject( const MerginApi *api, const QString &projectId ); //! Recursively deletes directory and its content. - void deleteLocalDir( MerginApi *api, const QString &dirPath ); + void deleteLocalDir( const MerginApi *api, const QString &dirPath ); //! Write all of "data" as the content to the given filename void writeFileContent( const QString &filename, const QByteArray &data ); @@ -218,7 +537,7 @@ class TestMerginApi: public QObject QByteArray readFileContent( const QString &filename ); //! Creates local project in given project directory - void createLocalProject( const QString projectDir ); + void createLocalProject( const QString &projectDir ); //! Creates json file based on params in path. Returns true is successful, false otherwise bool createJsonFile( const QString &path, const QVariantMap ¶ms ); diff --git a/app/test/testmodels.cpp b/app/test/testmodels.cpp index 456e203c9..0f0b8108e 100644 --- a/app/test/testmodels.cpp +++ b/app/test/testmodels.cpp @@ -15,6 +15,7 @@ #include "valuerelationfeaturesmodel.h" #include "projectsmodel.h" #include "projectsproxymodel.h" +#include "coreutils.h" #include @@ -304,16 +305,19 @@ void TestModels::testProjectsModel() Project p0; p0.local.projectNamespace = QStringLiteral( "namespace" ); p0.local.projectName = QStringLiteral( "project_B" ); + p0.local.projectId = CoreUtils::uuidWithoutBraces( QUuid::createUuid() ); p0.local.projectDir = QStringLiteral( "project_B_dir" ); Project p1; p1.local.projectNamespace = QStringLiteral( "namespace" ); p1.local.projectName = QStringLiteral( "project_A" ); + p1.local.projectId = CoreUtils::uuidWithoutBraces( QUuid::createUuid() ); p1.local.projectDir = QStringLiteral( "project_A_dir" ); Project p2; p2.local.projectNamespace = QStringLiteral( "namespace" ); p2.local.projectName = QStringLiteral( "project_C" ); + p2.local.projectId = CoreUtils::uuidWithoutBraces( QUuid::createUuid() ); p2.local.projectDir = QStringLiteral( "project_C_dir" ); ProjectsModel model; diff --git a/app/test/testutils.cpp b/app/test/testutils.cpp index 8bc279159..aceea7296 100644 --- a/app/test/testutils.cpp +++ b/app/test/testutils.cpp @@ -9,7 +9,6 @@ #include "QtDebug" #include -#include #include #include "testutils.h" @@ -74,7 +73,7 @@ void TestUtils::selectFirstWorkspace( MerginApi *api, QString &workspace ) } -bool TestUtils::needsToAuthorizeAgain( MerginApi *api, const QString &username ) +bool TestUtils::needsToAuthorizeAgain( const MerginApi *api, const QString &username ) { Q_ASSERT( api ); // no auth at all @@ -108,21 +107,21 @@ bool TestUtils::needsToAuthorizeAgain( MerginApi *api, const QString &username ) QString TestUtils::generateUsername() { - QDateTime time = QDateTime::currentDateTime(); - QString uniqename = time.toString( QStringLiteral( "ddMMyy-hhmmss-z" ) ); + const QDateTime time = QDateTime::currentDateTime(); + const QString uniqename = time.toString( QStringLiteral( "ddMMyy-hhmmss-z" ) ); return QStringLiteral( "input-%1" ).arg( uniqename ); } QString TestUtils::generateEmail() { - QDateTime time = QDateTime::currentDateTime(); - QString uniqename = time.toString( QStringLiteral( "ddMMyy-hhmmss-z" ) ); + const QDateTime time = QDateTime::currentDateTime(); + const QString uniqename = time.toString( QStringLiteral( "ddMMyy-hhmmss-z" ) ); return QStringLiteral( "mergin+autotest+%1@lutraconsulting.co.uk" ).arg( uniqename ); } QString TestUtils::generatePassword() { - QString pass = CoreUtils::uuidWithoutBraces( QUuid::createUuid() ).right( 15 ).replace( "-", "" ); + const QString pass = CoreUtils::uuidWithoutBraces( QUuid::createUuid() ).right( 15 ).replace( "-", "" ); return QStringLiteral( "_Pass12%1" ).arg( pass ); } @@ -140,7 +139,7 @@ bool TestUtils::generateProjectFolder( const QString &rootPath, const QJsonDocum if ( !QDir( rootPath ).exists() ) return false; - QJsonObject rootObj = structure.object(); + const QJsonObject rootObj = structure.object(); // generate files if ( rootObj.contains( "files" ) ) @@ -176,9 +175,9 @@ bool TestUtils::generateProjectFolder( const QString &rootPath, const QJsonDocum QgsProject *TestUtils::loadPlanesTestProject() { - QString projectDir = TestUtils::testDataDir() + "/planes"; - QString projectTempDir = QDir::tempPath() + "/" + QUuid::createUuid().toString(); - QString projectName = "quickapp_project.qgs"; + const QString projectDir = testDataDir() + "/planes"; + const QString projectTempDir = QDir::tempPath() + "/" + QUuid::createUuid().toString(); + const QString projectName = "quickapp_project.qgs"; // copy the project to tmp dir to not change its data InputUtils::cpDir( projectDir, projectTempDir ); @@ -195,18 +194,18 @@ void TestUtils::testLayerHasGeometry() QCOMPARE( InputUtils::layerHasGeometry( nullptr ), false ); // invalid layer => should be false - QgsVectorLayer *invalidLayer = new QgsVectorLayer( "", "InvalidLayer", "none" ); + const QgsVectorLayer *invalidLayer = new QgsVectorLayer( "", "InvalidLayer", "none" ); QVERIFY( invalidLayer->isValid() == false ); QCOMPARE( InputUtils::layerHasGeometry( invalidLayer ), false ); delete invalidLayer; // valid memory layer with geometry - QgsVectorLayer *pointLayer = new QgsVectorLayer( "Point?crs=EPSG:4326", "ValidPointLayer", "memory" ); + const QgsVectorLayer *pointLayer = new QgsVectorLayer( "Point?crs=EPSG:4326", "ValidPointLayer", "memory" ); QVERIFY( pointLayer->isValid() ); QCOMPARE( InputUtils::layerHasGeometry( pointLayer ), true ); // layer with NoGeo => should be false - QgsVectorLayer *noGeomLayer = new QgsVectorLayer( "None", "NoGeometryLayer", "memory" ); + const QgsVectorLayer *noGeomLayer = new QgsVectorLayer( "None", "NoGeometryLayer", "memory" ); QVERIFY( noGeomLayer->isValid() ); QCOMPARE( InputUtils::layerHasGeometry( noGeomLayer ), false ); @@ -234,7 +233,7 @@ void TestUtils::testLayerVisible() QCOMPARE( InputUtils::isLayerVisible( layer, project ), true ); // hide layer => false - QgsLayerTree *root = project->layerTreeRoot(); + const QgsLayerTree *root = project->layerTreeRoot(); QgsLayerTreeLayer *layerTree = root->findLayer( layer ); QVERIFY( layerTree ); layerTree->setItemVisibilityChecked( false ); @@ -253,7 +252,7 @@ void TestUtils::testIsPositionTrackingLayer() QCOMPARE( InputUtils::isPositionTrackingLayer( layer, project ), false ); // tracking layer ID => true - QString layerId = layer->id(); + const QString layerId = layer->id(); project->writeEntry( QStringLiteral( "Mergin" ), QStringLiteral( "PositionTracking/TrackingLayer" ), layerId ); QCOMPARE( InputUtils::isPositionTrackingLayer( layer, project ), true ); @@ -276,7 +275,7 @@ void TestUtils::testMapLayerFromName() QgsVectorLayer *layer = new QgsVectorLayer( "Point?crs=EPSG:4326", "MyTestLayer", "memory" ); QVERIFY( layer->isValid() ); project->addMapLayer( layer ); - QgsMapLayer *found = InputUtils::mapLayerFromName( "MyTestLayer", project ); + const QgsMapLayer *found = InputUtils::mapLayerFromName( "MyTestLayer", project ); QVERIFY( found != nullptr ); QCOMPARE( found->name(), QString( "MyTestLayer" ) ); @@ -300,4 +299,4 @@ void TestUtils::testIsValidUrl() QVERIFY( !InputUtils::isValidUrl( "://example.com" ) ); QVERIFY( !InputUtils::isValidUrl( "http://exa mple.com" ) ); QVERIFY( !InputUtils::isValidUrl( "" ) ); // empty url is considered valid by QUrl but not by us -} +} \ No newline at end of file diff --git a/app/test/testutils.h b/app/test/testutils.h index 5dbb4cd71..0a3633d51 100644 --- a/app/test/testutils.h +++ b/app/test/testutils.h @@ -11,17 +11,15 @@ #define TESTUTILS_H #include -#include -#include "inputconfig.h" #include "qgsproject.h" class MerginApi; namespace TestUtils { - const int SHORT_REPLY = 5000; - const int LONG_REPLY = 90000; + constexpr int SHORT_REPLY = 5000; + constexpr int LONG_REPLY = 90000; //! authorize user and select the active workspace void authorizeUser( MerginApi *api, const QString &username, const QString &password ); @@ -38,7 +36,7 @@ namespace TestUtils void merginGetAuthCredentials( MerginApi *api, QString &apiRoot, QString &username, QString &password ); //! Whether we need to auth again - bool needsToAuthorizeAgain( MerginApi *api, const QString &username ); + bool needsToAuthorizeAgain( const MerginApi *api, const QString &username ); QString generateUsername(); QString generateEmail(); @@ -62,6 +60,20 @@ namespace TestUtils void testIsPositionTrackingLayer(); void testMapLayerFromName(); void testIsValidUrl(); + + /** + * Function returns the project with same fullname. Expected types to pass are \a MerginProject & \a LocalProject. + */ + template + T findProjectByName( const QString &projectFullName, const QList &projects ) + { + for ( T project : projects ) + { + if ( project.fullName() == projectFullName ) + return project; + } + return T(); + }; } #define COMPARENEAR(actual, expected, epsilon) \ diff --git a/app/test/testutilsfunctions.cpp b/app/test/testutilsfunctions.cpp index 642fef562..a7f5915bb 100644 --- a/app/test/testutilsfunctions.cpp +++ b/app/test/testutilsfunctions.cpp @@ -823,7 +823,7 @@ void TestUtilsFunctions::testParsePositionUpdates() // example: "10 20 30 40\n" -> QgsPoint( x:10, y:20, z:30, m:40 ) // - QList>> testcases = + QList>> testcases = { { QString(), QList() }, { "", QList() }, diff --git a/app/variablesmanager.cpp b/app/variablesmanager.cpp index 375204f0a..5d57954dd 100644 --- a/app/variablesmanager.cpp +++ b/app/variablesmanager.cpp @@ -9,6 +9,7 @@ #include "variablesmanager.h" +#include "coreutils.h" #include "qgsexpressioncontextutils.h" #include "inputexpressionfunctions.h" @@ -19,9 +20,9 @@ VariablesManager::VariablesManager( MerginApi *merginApi, QObject *parent ) apiRootChanged(); setUserVariables(); - QObject::connect( mMerginApi, &MerginApi::apiRootChanged, this, &VariablesManager::apiRootChanged ); - QObject::connect( mMerginApi, &MerginApi::userInfoChanged, this, &VariablesManager::setUserVariables ); - QObject::connect( mMerginApi, &MerginApi::projectDataChanged, this, &VariablesManager::setVersionVariable ); + connect( mMerginApi, &MerginApi::apiRootChanged, this, &VariablesManager::apiRootChanged ); + connect( mMerginApi, &MerginApi::userInfoChanged, this, &VariablesManager::setUserVariables ); + connect( mMerginApi, &MerginApi::projectDataChanged, this, &VariablesManager::setVersionVariable ); } VariablesManager::~VariablesManager() = default; @@ -112,8 +113,10 @@ void VariablesManager::setUserVariables() QgsExpressionContextUtils::setGlobalVariable( QStringLiteral( "mergin_full_name" ), mMerginApi->userInfo()->name() ); } -void VariablesManager::setVersionVariable( const QString &projectFullName ) +void VariablesManager::setVersionVariable( const QString &projectFullName, const QString &projectId ) { + Q_UNUSED( projectId ); + if ( !mCurrentProject ) return; @@ -191,10 +194,10 @@ void VariablesManager::setProjectVariables() { QgsExpressionContextUtils::setProjectVariable( mCurrentProject, QStringLiteral( "mergin_project_version" ), metadata.version ); QgsExpressionContextUtils::setProjectVariable( mCurrentProject, QStringLiteral( "mergin_project_name" ), metadata.name ); - QgsExpressionContextUtils::setProjectVariable( mCurrentProject, QStringLiteral( "mergin_project_full_name" ), mMerginApi->getFullProjectName( metadata.projectNamespace, metadata.name ) ); + QgsExpressionContextUtils::setProjectVariable( mCurrentProject, QStringLiteral( "mergin_project_full_name" ), CoreUtils::getFullProjectName( metadata.projectNamespace, metadata.name ) ); QgsExpressionContextUtils::setProjectVariable( mCurrentProject, QStringLiteral( "mm_project_version" ), metadata.version ); QgsExpressionContextUtils::setProjectVariable( mCurrentProject, QStringLiteral( "mm_project_name" ), metadata.name ); - QgsExpressionContextUtils::setProjectVariable( mCurrentProject, QStringLiteral( "mm_project_full_name" ), mMerginApi->getFullProjectName( metadata.projectNamespace, metadata.name ) ); + QgsExpressionContextUtils::setProjectVariable( mCurrentProject, QStringLiteral( "mm_project_full_name" ), CoreUtils::getFullProjectName( metadata.projectNamespace, metadata.name ) ); } else { diff --git a/app/variablesmanager.h b/app/variablesmanager.h index 270ad50af..014192670 100644 --- a/app/variablesmanager.h +++ b/app/variablesmanager.h @@ -65,7 +65,7 @@ class VariablesManager : public QObject private slots: void apiRootChanged(); void setUserVariables(); - void setVersionVariable( const QString &projectFullName ); + void setVersionVariable( const QString &projectFullName, const QString &projectId ); private: MerginApi *mMerginApi = nullptr; diff --git a/core/coreutils.cpp b/core/coreutils.cpp index 39255c362..90eed6a0f 100644 --- a/core/coreutils.cpp +++ b/core/coreutils.cpp @@ -10,16 +10,9 @@ #include "coreutils.h" #include "inputconfig.h" -#include -#include -#include #include -#include -#include -#include #include #include -#include #include #include #include @@ -29,7 +22,7 @@ const QString CoreUtils::QSETTINGS_APP_GROUP_NAME = QStringLiteral( "inputApp" ); const QString CoreUtils::LOG_TO_DEVNULL = QStringLiteral(); const QString CoreUtils::LOG_TO_STDOUT = QStringLiteral( "TO_STDOUT" ); -QString CoreUtils::sLogFile = CoreUtils::LOG_TO_DEVNULL; +QString CoreUtils::sLogFile = LOG_TO_DEVNULL; int CoreUtils::CHECKSUM_CHUNK_SIZE = 65536; QString CoreUtils::deviceUuid() @@ -38,11 +31,11 @@ QString CoreUtils::deviceUuid() QSettings settings; settings.beginGroup( QSETTINGS_APP_GROUP_NAME ); - QVariant uuidEntry = settings.value( "deviceUuid" ); + const QVariant uuidEntry = settings.value( "deviceUuid" ); if ( uuidEntry.isNull() ) { uuid = uuidWithoutBraces( QUuid::createUuid() ); - CoreUtils::log( QStringLiteral( "Device" ), QStringLiteral( "deviceUuid generated: %1" ).arg( uuid ) ); + log( QStringLiteral( "Device" ), QStringLiteral( "deviceUuid generated: %1" ).arg( uuid ) ); settings.setValue( "deviceUuid", uuid ); } else @@ -79,15 +72,15 @@ QString CoreUtils::appVersionCode() return version; } -QString CoreUtils::localizedDateFromUTFString( QString timestamp ) +QString CoreUtils::localizedDateFromUTFString( const QString ×tamp ) { if ( timestamp.isEmpty() ) return QString(); - QDateTime dateTime = QDateTime::fromString( timestamp, Qt::ISODate ); + const QDateTime dateTime = QDateTime::fromString( timestamp, Qt::ISODate ); if ( dateTime.isValid() ) { - QLocale locale = QLocale::system(); + const QLocale locale = QLocale::system(); return locale.toString( dateTime.date(), locale.dateFormat( QLocale::ShortFormat ) ); } else @@ -108,12 +101,12 @@ QString CoreUtils::uuidWithoutBraces( const QUuid &uuid ) #endif } -bool CoreUtils::removeDir( const QString &dir ) +bool CoreUtils::removeDir( const QString &projectDir ) { - if ( dir.isEmpty() || dir == "/" ) + if ( projectDir.isEmpty() || projectDir == "/" ) return false; - return QDir( dir ).removeRecursively(); + return QDir( projectDir ).removeRecursively(); } QString CoreUtils::downloadInProgressFilePath( const QString &projectDir ) @@ -136,7 +129,9 @@ void CoreUtils::log( const QString &topic, const QString &info ) { QString logFilePath; QByteArray data; - data.append( QString( "%1 %2: %3\n" ).arg( QDateTime().currentDateTimeUtc().toString( Qt::ISODateWithMs ) ).arg( topic ).arg( info ).toUtf8() ); + data.append( + QString( "%1 %2: %3\n" ).arg( QDateTime().currentDateTimeUtc().toString( Qt::ISODateWithMs ) ).arg( topic ) + .arg( info ).toUtf8() ); appendLog( data, sLogFile ); } @@ -166,11 +161,11 @@ void CoreUtils::appendLog( const QByteArray &data, const QString &path ) QString CoreUtils::findUniquePath( const QString &path ) { - QFileInfo originalPath( path ); + const QFileInfo originalPath( path ); QString uniquePath = path; // are we dealing with directory? - bool isDirectory = originalPath.isDir(); + const bool isDirectory = originalPath.isDir(); int i = 0; QFileInfo f( uniquePath ); @@ -215,10 +210,10 @@ QByteArray CoreUtils::calculateChecksum( const QString &filePath ) QString CoreUtils::createUniqueProjectDirectory( const QString &baseDataDir, const QString &projectName ) { QString projectDirPath = findUniquePath( baseDataDir + "/" + projectName ); - QDir projectDir( projectDirPath ); + const QDir projectDir( projectDirPath ); if ( !projectDir.exists() ) { - QDir dir( "" ); + const QDir dir( "" ); dir.mkdir( projectDirPath ); } return projectDirPath; @@ -234,12 +229,12 @@ bool CoreUtils::createEmptyFile( const QString &filePath ) return true; } -QString CoreUtils::generateConflictedCopyFileName( const QString &file, const QString &username, int version ) +QString CoreUtils::generateConflictedCopyFileName( const QString &file, const QString &username, const int version ) { if ( file.isEmpty() ) return QString(); - QFileInfo f( file ); + const QFileInfo f( file ); QString suffix = f.completeSuffix(); if ( hasProjectFileExtension( file ) ) @@ -249,24 +244,24 @@ QString CoreUtils::generateConflictedCopyFileName( const QString &file, const QS return QString( "%1/%2 (conflicted copy, %3 v%4).%5" ).arg( f.path(), f.baseName(), username, QString::number( version ).toUtf8(), suffix ); } -QString CoreUtils::generateEditConflictFileName( const QString &file, const QString &username, int version ) +QString CoreUtils::generateEditConflictFileName( const QString &file, const QString &username, const int version ) { if ( file.isEmpty() ) return QString(); - QFileInfo f( file ); + const QFileInfo f( file ); return QString( "%1/%2 (edit conflict, %3 v%4).json" ).arg( f.path(), f.baseName(), username, QString::number( version ) ); } -bool CoreUtils::hasProjectFileExtension( const QString filePath ) +bool CoreUtils::hasProjectFileExtension( const QString &filePath ) { return filePath.contains( ".qgs", Qt::CaseInsensitive ) || filePath.contains( ".qgz", Qt::CaseInsensitive ); } bool CoreUtils::isValidName( const QString &name ) { - static QRegularExpression reForbiddenmNames( R"([@#$%^&*\(\)\{\}\[\]\\\/\|\+=<>~\?:;,`\'\"]|^[\s^\.].*$|^CON$|^PRN$|^AUX$|^NUL$|^COM\d$|^LPT\d|^support$|^helpdesk$|^merginmaps$|^lutraconsulting$|^mergin$|^lutra$|^input$|^sales$|^admin$)", QRegularExpression::CaseInsensitiveOption ); - QRegularExpressionMatch matchForbiddenNames = reForbiddenmNames.match( name ); + static QRegularExpression reForbiddenNames( R"([@#$%^&*\(\)\{\}\[\]\\\/\|\+=<>~\?:;,`\'\"]|^[\s^\.].*$|^CON$|^PRN$|^AUX$|^NUL$|^COM\d$|^LPT\d|^support$|^helpdesk$|^merginmaps$|^lutraconsulting$|^mergin$|^lutra$|^input$|^sales$|^admin$)", QRegularExpression::CaseInsensitiveOption ); + const QRegularExpressionMatch matchForbiddenNames = reForbiddenNames.match( name ); return !matchForbiddenNames.hasMatch(); } @@ -289,8 +284,8 @@ QString CoreUtils::nameAbbr( const QString &name, const QString &email ) QString CoreUtils::getAvailableDeviceStorage() { - QString appDir = QCoreApplication::applicationDirPath(); - QStorageInfo storageInfo( appDir ); + const QString appDir = QCoreApplication::applicationDirPath(); + const QStorageInfo storageInfo( appDir ); if ( storageInfo.isValid() && storageInfo.isReady() ) { @@ -302,8 +297,8 @@ QString CoreUtils::getAvailableDeviceStorage() QString CoreUtils::getTotalDeviceStorage() { - QString appDir = QCoreApplication::applicationDirPath(); - QStorageInfo storageInfo( appDir ); + const QString appDir = QCoreApplication::applicationDirPath(); + const QStorageInfo storageInfo( appDir ); if ( storageInfo.isValid() && storageInfo.isReady() ) { @@ -313,32 +308,29 @@ QString CoreUtils::getTotalDeviceStorage() return "N/A"; } -QString CoreUtils::bytesToHumanSize( double bytes ) +QString CoreUtils::bytesToHumanSize( const double bytes ) { - const int precision = 1; + constexpr int precision = 1; if ( bytes < 1e-5 ) { return "0.0"; } - else if ( bytes < 1024.0 * 1024.0 ) + if ( bytes < 1024.0 * 1024.0 ) { return QString::number( bytes / 1024.0, 'f', precision ) + " KB"; } - else if ( bytes < 1024.0 * 1024.0 * 1024.0 ) + if ( bytes < 1024.0 * 1024.0 * 1024.0 ) { return QString::number( bytes / 1024.0 / 1024.0, 'f', precision ) + " MB"; } - else if ( bytes < 1024.0 * 1024.0 * 1024.0 * 1024.0 ) + if ( bytes < 1024.0 * 1024.0 * 1024.0 * 1024.0 ) { return QString::number( bytes / 1024.0 / 1024.0 / 1024.0, 'f', precision ) + " GB"; } - else - { - return QString::number( bytes / 1024.0 / 1024.0 / 1024.0 / 1024.0, 'f', precision ) + " TB"; - } + return QString::number( bytes / 1024.0 / 1024.0 / 1024.0 / 1024.0, 'f', precision ) + " TB"; } -QString CoreUtils::getProjectMetadataPath( QString projectDir ) +QString CoreUtils::getProjectMetadataPath( const QString &projectDir ) { if ( projectDir.isEmpty() ) return QString(); @@ -354,7 +346,7 @@ bool CoreUtils::replaceValueInJson( const QString &filePath, const QString &key, return false; } - QByteArray data = file.readAll(); + const QByteArray data = file.readAll(); file.close(); QJsonDocument doc = QJsonDocument::fromJson( data ); @@ -372,12 +364,30 @@ bool CoreUtils::replaceValueInJson( const QString &filePath, const QString &key, return false; } - bool success = ( file.write( doc.toJson() ) != -1 ); + const bool success = file.write( doc.toJson() ) != -1; file.close(); return success; } +QString CoreUtils::getFullProjectName( const QString &projectNamespace, const QString &projectName ) +{ + return QString( "%1/%2" ).arg( projectNamespace ).arg( projectName ); +} + +bool CoreUtils::extractProjectName( const QString &sourceString, QString &projectNamespace, QString &projectName ) +{ + QStringList parts = sourceString.split( "/" ); + if ( parts.length() > 1 ) + { + projectNamespace = parts.at( parts.length() - 2 ); + projectName = parts.last(); + return true; + } + projectName = sourceString; + return false; +} + bool CoreUtils::isValidEmail( const QString &email ) { const thread_local QRegularExpression regEx( "\\S+@\\S+\\.\\S+" ); diff --git a/core/coreutils.h b/core/coreutils.h index 6740a6113..92a89d432 100644 --- a/core/coreutils.h +++ b/core/coreutils.h @@ -14,8 +14,6 @@ #define STR(x) STR1(x) #include -#include -#include class CoreUtils @@ -28,12 +26,11 @@ class CoreUtils static QString appVersion(); static QString appVersionCode(); - static QString localizedDateFromUTFString( QString timestamp ); + static QString localizedDateFromUTFString( const QString ×tamp ); static bool removeDir( const QString &projectDir ); /** * Returns name of temporary file indicating first time download of project is in progress - * \param projectName */ static QString downloadInProgressFilePath( const QString &projectDir ); @@ -86,10 +83,10 @@ class CoreUtils static void log( const QString &topic, const QString &info ); //! Checks whether file path has a QGIS project suffix (qgs or qgz) - static bool hasProjectFileExtension( const QString filePath ); + static bool hasProjectFileExtension( const QString &filePath ); /** - * Check whether given project/user name is valid + * Check whether given project/username is valid */ static bool isValidName( const QString &name ); @@ -121,20 +118,34 @@ class CoreUtils static QString getTotalDeviceStorage(); /** - * Converts bytes to human readable size (e.g. 1GB, 500MB) + * Converts bytes to human-readable size (e.g. 1GB, 500MB) */ static QString bytesToHumanSize( double bytes ); /** * Returns path to project metadata file for a given project directory */ - static QString getProjectMetadataPath( QString projectDir ); + static QString getProjectMetadataPath( const QString &projectDir ); /** * Updates a value in a JSON file at the specified top-level key */ static bool replaceValueInJson( const QString &filePath, const QString &key, const QJsonValue &value ); + /** + * Creates the full project name, which essentially means joining \a projectNamespace and \a projectName with "/" + */ + static QString getFullProjectName( const QString &projectNamespace, const QString &projectName ); + + /** + * Sets projectNamespace and projectName from sourceString - url or any string from which takes last (name) + * and the previous of last (namespace) substring after splitting sourceString with slash. + * \param sourceString QString either url or fullname of a project + * \param projectNamespace QString to be set as namespace, might not change original value + * \param projectName QString to be set to name of a project + */ + static bool extractProjectName( const QString &sourceString, QString &projectNamespace, QString &projectName ); + /** * We do some very basic checks if the string looks like email. */ diff --git a/core/localprojectsmanager.cpp b/core/localprojectsmanager.cpp index f3d134fc8..557c52ffd 100644 --- a/core/localprojectsmanager.cpp +++ b/core/localprojectsmanager.cpp @@ -38,98 +38,80 @@ void LocalProjectsManager::reloadDataDir() info.projectName = metadata.name; info.projectNamespace = metadata.projectNamespace; info.localVersion = metadata.version; + info.projectId = metadata.projectId; } else { info.projectName = folderName; + info.projectId = LocalProject::generateProjectId(); } - mProjects << info; + mProjects.insert( info.projectId, info ); } - QString msg = QString( "Found %1 local projects in %2" ).arg( mProjects.size() ).arg( mDataDir ); + const QString msg = QString( "Found %1 local projects in %2" ).arg( mProjects.size() ).arg( mDataDir ); CoreUtils::log( "Local projects", msg ); emit dataDirReloaded(); } LocalProject LocalProjectsManager::projectFromDirectory( const QString &projectDir ) const { - for ( const LocalProject &info : mProjects ) + for ( const LocalProject &info : mProjects.values() ) { if ( info.projectDir == projectDir ) return info; } - return LocalProject(); + return {}; } LocalProject LocalProjectsManager::projectFromProjectFilePath( const QString &projectFilePath ) const { - for ( const LocalProject &info : mProjects ) + for ( const LocalProject &info : mProjects.values() ) { if ( info.qgisProjectFilePath == projectFilePath ) return info; } - return LocalProject(); + return {}; } LocalProject LocalProjectsManager::projectFromProjectId( const QString &projectId ) const { - for ( const LocalProject &info : mProjects ) + if ( mProjects.contains( projectId ) ) { - if ( info.id() == projectId ) - return info; - } - return LocalProject(); -} - -LocalProject LocalProjectsManager::projectFromMerginName( const QString &projectFullName ) const -{ - for ( const LocalProject &info : mProjects ) - { - if ( info.id() == projectFullName ) - return info; + return mProjects.value( projectId ); } - return LocalProject(); -} - -LocalProject LocalProjectsManager::projectFromMerginName( const QString &projectNamespace, const QString &projectName ) const -{ - return projectFromMerginName( MerginApi::getFullProjectName( projectNamespace, projectName ) ); + return {}; } void LocalProjectsManager::addLocalProject( const QString &projectDir, const QString &projectName ) { - addProject( projectDir, QString(), projectName ); + addProject( projectDir, QString(), projectName, LocalProject::generateProjectId() ); } -void LocalProjectsManager::addMerginProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ) +void LocalProjectsManager::addMerginProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName, const QString &projectId ) { - addProject( projectDir, projectNamespace, projectName ); + addProject( projectDir, projectNamespace, projectName, projectId ); } void LocalProjectsManager::removeLocalProject( const QString &projectId ) { - for ( int i = 0; i < mProjects.count(); ++i ) + if ( mProjects.contains( projectId ) ) { - if ( mProjects[i].id() == projectId ) - { - emit aboutToRemoveLocalProject( mProjects[i] ); - - CoreUtils::removeDir( mProjects[i].projectDir ); - mProjects.removeAt( i ); + const LocalProject project = mProjects.value( projectId ); + emit aboutToRemoveLocalProject( project ); - return; - } + CoreUtils::removeDir( project.projectDir ); + mProjects.remove( projectId ); } } bool LocalProjectsManager::projectIsValid( const QString &path ) const { - for ( int i = 0; i < mProjects.count(); ++i ) + for ( LocalProject &project : mProjects.values() ) { - if ( mProjects[i].qgisProjectFilePath == path ) + if ( project.qgisProjectFilePath == path ) { - return mProjects[i].projectError.isEmpty(); + return project.projectError.isEmpty(); } } return false; @@ -137,66 +119,55 @@ bool LocalProjectsManager::projectIsValid( const QString &path ) const QString LocalProjectsManager::projectId( const QString &path ) const { - for ( int i = 0; i < mProjects.count(); ++i ) + for ( LocalProject &project : mProjects.values() ) { - if ( mProjects[i].qgisProjectFilePath == path ) + if ( project.qgisProjectFilePath == path ) { - return mProjects[i].id(); + return project.id(); } } - return QString(); + return {}; } QString LocalProjectsManager::projectName( const QString &projectId ) const { - LocalProject project = projectFromProjectId( projectId ); + const LocalProject project = projectFromProjectId( projectId ); if ( project.isValid() ) { - return MerginApi::getFullProjectName( project.projectNamespace, project.projectName ); + return CoreUtils::getFullProjectName( project.projectNamespace, project.projectName ); } - return QString(); + return {}; } -QString LocalProjectsManager::projectChanges( const QString &projectId ) +QString LocalProjectsManager::projectChanges( const QString &projectId ) const { - LocalProject project = projectFromProjectId( projectId ); + const LocalProject project = projectFromProjectId( projectId ); if ( project.isValid() ) { return MerginApi::localProjectChanges( project.projectDir ).dump(); } - return QString(); + return {}; } -void LocalProjectsManager::updateLocalVersion( const QString &projectDir, int version ) +void LocalProjectsManager::updateLocalVersion( const QString &projectId, const int version ) { - for ( int i = 0; i < mProjects.count(); ++i ) + if ( mProjects.contains( projectId ) ) { - if ( mProjects[i].projectDir == projectDir ) - { - mProjects[i].localVersion = version; - - emit localProjectDataChanged( mProjects[i] ); - return; - } + mProjects[ projectId ].localVersion = version; + emit localProjectDataChanged( mProjects.value( projectId ) ); } - Q_ASSERT( false ); // should not happen } -void LocalProjectsManager::updateNamespace( const QString &projectDir, const QString &projectNamespace ) +void LocalProjectsManager::updateNamespace( const QString &projectId, const QString &projectNamespace ) { - for ( int i = 0; i < mProjects.count(); ++i ) + if ( mProjects.contains( projectId ) ) { - if ( mProjects[i].projectDir == projectDir ) - { - mProjects[i].projectNamespace = projectNamespace; - - emit localProjectDataChanged( mProjects[i] ); - return; - } + mProjects[ projectId ].projectNamespace = projectNamespace; + emit localProjectDataChanged( mProjects.value( projectId ) ); } } @@ -208,7 +179,7 @@ QString LocalProjectsManager::findQgisProjectFile( const QString &projectDir, QS // download failed or copying from .temp to project dir failed (app was probably closed meanwhile) err = tr( "Download failed, remove and retry" ); - return QString(); + return {}; } QList foundProjectFiles; @@ -235,17 +206,33 @@ QString LocalProjectsManager::findQgisProjectFile( const QString &projectDir, QS err = tr( "Failed to find a QGIS project file" ); } - return QString(); + return {}; +} + +void LocalProjectsManager::updateProjectId( const QString &oldProjectId, const QString &newProjectId ) +{ + if ( mProjects.contains( oldProjectId ) ) + { + // updating values just in LocalProjectsManager is not enough we also update ProjectsModel and ActiveProject + LocalProject project = mProjects.value( oldProjectId ); + emit aboutToRemoveLocalProject( project ); + mProjects.remove( oldProjectId ); + project.projectId = newProjectId; + mProjects.insert( newProjectId, project ); + emit localProjectAdded( project ); + emit localProjectDataChanged( project ); + } } -void LocalProjectsManager::addProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ) +void LocalProjectsManager::addProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName, const QString &projectId ) { LocalProject project; project.projectDir = projectDir; project.qgisProjectFilePath = findQgisProjectFile( projectDir, project.projectError ); project.projectName = projectName; project.projectNamespace = projectNamespace; + project.projectId = projectId; - mProjects << project; + mProjects.insert( projectId, project ); emit localProjectAdded( project ); } diff --git a/core/localprojectsmanager.h b/core/localprojectsmanager.h index f90290861..bed49642b 100644 --- a/core/localprojectsmanager.h +++ b/core/localprojectsmanager.h @@ -10,7 +10,6 @@ #ifndef LOCALPROJECTSMANAGER_H #define LOCALPROJECTSMANAGER_H -#include #include "project.h" /** @@ -29,20 +28,17 @@ class LocalProjectsManager : public QObject QString dataDir() const { return mDataDir; } - LocalProjectsList projects() const { return mProjects; } + LocalProjectsDict projects() const { return mProjects; } LocalProject projectFromDirectory( const QString &projectDir ) const; LocalProject projectFromProjectFilePath( const QString &projectFilePath ) const; LocalProject projectFromProjectId( const QString &projectId ) const; - LocalProject projectFromMerginName( const QString &projectFullName ) const; - LocalProject projectFromMerginName( const QString &projectNamespace, const QString &projectName ) const; - //! Adds entry about newly created project void addLocalProject( const QString &projectDir, const QString &projectName ); //! Adds entry for downloaded project - void addMerginProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ); + void addMerginProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName, const QString &projectId ); //! Should forget about that project (it has been removed already) Q_INVOKABLE void removeLocalProject( const QString &projectId ); @@ -57,17 +53,20 @@ class LocalProjectsManager : public QObject * Returns changes of a project specified by projectId in the form : * (pending changes, features in layer survey: 10 addition, 3 updates, 1 deletion. 10 new files) */ - Q_INVOKABLE QString projectChanges( const QString &projectId ); + Q_INVOKABLE QString projectChanges( const QString &projectId ) const; //! after successful update/upload - both server and local version are the same - void updateLocalVersion( const QString &projectDir, int version ); + void updateLocalVersion( const QString &projectId, int version ); //! Updates project's namespace - void updateNamespace( const QString &projectDir, const QString &projectNamespace ); + void updateNamespace( const QString &projectId, const QString &projectNamespace ); - //! Finds all QGIS project files and set the err variable if any occured. + //! Finds all QGIS project files and set the err variable if any occurred. QString findQgisProjectFile( const QString &projectDir, QString &err ); + //! Updates project's ID + void updateProjectId( const QString &oldProjectId, const QString &newProjectId ); + signals: void localProjectAdded( const LocalProject &project ); void localProjectDataChanged( const LocalProject &project ); @@ -76,10 +75,10 @@ class LocalProjectsManager : public QObject void dataDirReloaded(); private: - void addProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName ); + void addProject( const QString &projectDir, const QString &projectNamespace, const QString &projectName, const QString &projectId ); QString mDataDir; //!< directory with all local projects - LocalProjectsList mProjects; + LocalProjectsDict mProjects; }; diff --git a/core/merginapi.cpp b/core/merginapi.cpp index ae58d4134..8d641ebe5 100644 --- a/core/merginapi.cpp +++ b/core/merginapi.cpp @@ -9,7 +9,6 @@ #include "merginapi.h" -#include #include #include #include @@ -19,6 +18,7 @@ #include #include #include +#include #include #ifdef MOBILE_OS #include @@ -34,6 +34,7 @@ #include "merginerrortypes.h" #include +#include const QString MerginApi::sMetadataFile = QStringLiteral( "/.mergin/mergin.json" ); const QString MerginApi::sMetadataFolder = QStringLiteral( ".mergin" ); @@ -64,7 +65,7 @@ MerginApi::MerginApi( LocalProjectsManager &localProjects, QObject *parent ) , mManager( new QNetworkAccessManager( this ) ) { // load cached data if there are any - QSettings cache; + const QSettings cache; if ( cache.contains( QStringLiteral( "Input/apiRoot" ) ) ) { loadCache(); @@ -77,12 +78,12 @@ MerginApi::MerginApi( LocalProjectsManager &localProjects, QObject *parent ) qRegisterMetaType(); - QObject::connect( this, &MerginApi::authChanged, this, &MerginApi::saveAuthData ); - QObject::connect( this, &MerginApi::apiRootChanged, this, &MerginApi::pingMergin ); - QObject::connect( this, &MerginApi::apiRootChanged, this, &MerginApi::getServerConfig ); - QObject::connect( this, &MerginApi::pingMerginFinished, this, &MerginApi::checkMerginVersion ); - QObject::connect( this, &MerginApi::workspaceCreated, this, &MerginApi::getUserInfo ); - QObject::connect( this, &MerginApi::serverTypeChanged, this, [this]() + connect( this, &MerginApi::authChanged, this, &MerginApi::saveAuthData ); + connect( this, &MerginApi::apiRootChanged, this, &MerginApi::pingMergin ); + connect( this, &MerginApi::apiRootChanged, this, &MerginApi::getServerConfig ); + connect( this, &MerginApi::pingMerginFinished, this, &MerginApi::checkMerginVersion ); + connect( this, &MerginApi::workspaceCreated, this, &MerginApi::getUserInfo ); + connect( this, &MerginApi::serverTypeChanged, this, [this] { if ( mUserAuth->hasAuthData() ) { @@ -90,16 +91,16 @@ MerginApi::MerginApi( LocalProjectsManager &localProjects, QObject *parent ) getUserInfo(); } } ); - QObject::connect( this, &MerginApi::processInvitationFinished, this, &MerginApi::getUserInfo ); - QObject::connect( this, &MerginApi::getWorkspaceInfoFinished, this, &MerginApi::getServiceInfo ); - QObject::connect( mUserInfo, &MerginUserInfo::userInfoChanged, this, &MerginApi::userInfoChanged ); - QObject::connect( mUserInfo, &MerginUserInfo::activeWorkspaceChanged, this, &MerginApi::activeWorkspaceChanged ); - QObject::connect( mUserInfo, &MerginUserInfo::activeWorkspaceChanged, this, &MerginApi::getWorkspaceInfo ); - QObject::connect( mUserInfo, &MerginUserInfo::hasWorkspacesChanged, this, &MerginApi::hasWorkspacesChanged ); - QObject::connect( mSubscriptionInfo, &MerginSubscriptionInfo::subscriptionInfoChanged, this, &MerginApi::subscriptionInfoChanged ); - QObject::connect( mSubscriptionInfo, &MerginSubscriptionInfo::planProductIdChanged, this, &MerginApi::onPlanProductIdChanged ); - QObject::connect( mUserAuth, &MerginUserAuth::authChanged, this, &MerginApi::authChanged ); - QObject::connect( mUserAuth, &MerginUserAuth::authChanged, this, [this]() + connect( this, &MerginApi::processInvitationFinished, this, &MerginApi::getUserInfo ); + connect( this, &MerginApi::getWorkspaceInfoFinished, this, &MerginApi::getServiceInfo ); + connect( mUserInfo, &MerginUserInfo::userInfoChanged, this, &MerginApi::userInfoChanged ); + connect( mUserInfo, &MerginUserInfo::activeWorkspaceChanged, this, &MerginApi::activeWorkspaceChanged ); + connect( mUserInfo, &MerginUserInfo::activeWorkspaceChanged, this, &MerginApi::getWorkspaceInfo ); + connect( mUserInfo, &MerginUserInfo::hasWorkspacesChanged, this, &MerginApi::hasWorkspacesChanged ); + connect( mSubscriptionInfo, &MerginSubscriptionInfo::subscriptionInfoChanged, this, &MerginApi::subscriptionInfoChanged ); + connect( mSubscriptionInfo, &MerginSubscriptionInfo::planProductIdChanged, this, &MerginApi::onPlanProductIdChanged ); + connect( mUserAuth, &MerginUserAuth::authChanged, this, &MerginApi::authChanged ); + connect( mUserAuth, &MerginUserAuth::authChanged, this, [this] { if ( mUserAuth->hasValidToken() ) { @@ -120,14 +121,14 @@ MerginApi::MerginApi( LocalProjectsManager &localProjects, QObject *parent ) if ( mUserAuth->hasAuthData() ) { - QObject::connect( this, &MerginApi::pingMerginFinished, this, &MerginApi::getUserInfo, Qt::SingleShotConnection ); - QObject::connect( this, &MerginApi::userInfoReplyFinished, this, &MerginApi::getWorkspaceInfo, Qt::SingleShotConnection ); + connect( this, &MerginApi::pingMerginFinished, this, &MerginApi::getUserInfo, Qt::SingleShotConnection ); + connect( this, &MerginApi::userInfoReplyFinished, this, &MerginApi::getWorkspaceInfo, Qt::SingleShotConnection ); } } void MerginApi::loadCache() { - QSettings settings; + const QSettings settings; setApiRoot( settings.value( QStringLiteral( "Input/apiRoot" ) ).toString() ); int serverType = settings.value( QStringLiteral( "Input/serverType" ) ).toInt(); @@ -159,12 +160,12 @@ MerginSubscriptionInfo *MerginApi::subscriptionInfo() const QString MerginApi::listProjects( const QString &searchExpression, const QString &flag, const int page ) { - bool authorize = flag != "public"; + const bool authorize = flag != "public"; if ( ( authorize && !validateAuth() ) || mApiVersionStatus != MerginApiStatus::OK ) { emit listProjectsFailed(); - return QString(); + return {}; } QUrlQuery query; @@ -174,7 +175,7 @@ QString MerginApi::listProjects( const QString &searchExpression, const QString if ( mUserInfo->activeWorkspaceId() < 0 ) { emit listProjectsFailed(); - return QString(); + return {}; } query.addQueryItem( "only_namespace", mUserInfo->activeWorkspaceName() ); @@ -206,15 +207,15 @@ QString MerginApi::listProjects( const QString &searchExpression, const QString QUrl url( mApiRoot + QStringLiteral( "/v1/project/paginated" ) ); url.setQuery( query ); - // Even if the authorization is not required, it can be include to fetch more results + // Even if the authorization is not required, it can be included to fetch more results QNetworkRequest request = getDefaultRequest( mUserAuth->hasAuthData() ); request.setUrl( url ); QString requestId = CoreUtils::uuidWithoutBraces( QUuid::createUuid() ); - QNetworkReply *reply = mManager->get( request ); + const QNetworkReply *reply = mManager->get( request ); CoreUtils::log( "list projects", QStringLiteral( "Requesting: " ) + url.toString() ); - connect( reply, &QNetworkReply::finished, this, [this, requestId]() {this->listProjectsReplyFinished( requestId );} ); + connect( reply, &QNetworkReply::finished, this, [this, requestId] {this->listProjectsReplyFinished( requestId );} ); return requestId; } @@ -227,17 +228,16 @@ QString MerginApi::listProjectsByName( const QStringList &projectNames ) return QLatin1String(); } - const int listProjectsByNameApiLimit = 50; + constexpr int maxProjectRequests = 50; QStringList projectNamesToRequest( projectNames ); - if ( projectNamesToRequest.count() > listProjectsByNameApiLimit ) + if ( projectNamesToRequest.count() > maxProjectRequests ) { - CoreUtils::log( "list projects by name", QStringLiteral( "Too many local projects: " ) + QString::number( projectNames.count(), 'f', 0 ) ); - const int projectsToRemoveCount = projectNames.count() - listProjectsByNameApiLimit; - QString msg = tr( "Please remove some projects as the app currently\nonly allows up to %1 downloaded projects." ).arg( listProjectsByNameApiLimit ); + CoreUtils::log( "list projects by name", QStringLiteral( "Too many local projects: " ) + QString::number( static_cast( projectNames.count() ), 'f', 0 ) ); + const QString msg = tr( "Please remove some projects as the app currently\nonly allows up to %1 downloaded projects." ).arg( maxProjectRequests ); notifyInfo( msg ); - projectNamesToRequest.erase( projectNamesToRequest.begin() + listProjectsByNameApiLimit, projectNamesToRequest.end() ); - Q_ASSERT( projectNamesToRequest.count() == listProjectsByNameApiLimit ); + projectNamesToRequest.erase( projectNamesToRequest.begin() + maxProjectRequests, projectNamesToRequest.end() ); + Q_ASSERT( projectNamesToRequest.count() == maxProjectRequests ); } // Authentification is optional in this case, as there might be public projects without the need to be logged in. @@ -248,12 +248,12 @@ QString MerginApi::listProjectsByName( const QStringList &projectNames ) // construct JSON body QJsonDocument body; QJsonObject projects; - QJsonArray projectsArr = QJsonArray::fromStringList( projectNamesToRequest ); + const QJsonArray projectsArr = QJsonArray::fromStringList( projectNamesToRequest ); projects.insert( "projects", projectsArr ); body.setObject( projects ); - QUrl url( mApiRoot + QStringLiteral( "/v1/project/by_names" ) ); + const QUrl url( mApiRoot + QStringLiteral( "/v1/project/by_names" ) ); QNetworkRequest request = getDefaultRequest( true ); request.setUrl( url ); @@ -261,18 +261,26 @@ QString MerginApi::listProjectsByName( const QStringList &projectNames ) QString requestId = CoreUtils::uuidWithoutBraces( QUuid::createUuid() ); - QNetworkReply *reply = mManager->post( request, body.toJson() ); + const QNetworkReply *reply = mManager->post( request, body.toJson() ); CoreUtils::log( "list projects by name", QStringLiteral( "Requesting: " ) + url.toString() ); - connect( reply, &QNetworkReply::finished, this, [this, requestId]() {this->listProjectsByNameReplyFinished( requestId );} ); + connect( reply, &QNetworkReply::finished, this, [this, requestId] {this->listProjectsByNameReplyFinished( requestId );} ); return requestId; } +void MerginApi::refetchBrokenProjects( const QStringList &projectIds ) +{ + const QNetworkReply *reply = getProjectsDetails( projectIds ); + connect( reply, &QNetworkReply::finished, this, &MerginApi::refetchBrokenProjectsReplyFinished ); +} + -void MerginApi::downloadNextItem( const QString &projectFullName ) +void MerginApi::downloadNextItem( const QString &projectId ) { - Q_ASSERT( mTransactionalStatus.contains( projectFullName ) ); - TransactionStatus &transaction = mTransactionalStatus[projectFullName]; + Q_ASSERT( mTransactionalStatus.contains( projectId ) ); + TransactionStatus &transaction = mTransactionalStatus[projectId]; + const MerginProjectMetadata project = MerginProjectMetadata::fromJson( transaction.projectMetadata ); + const QString projectFullName = CoreUtils::getFullProjectName( project.projectNamespace, project.name ); Q_ASSERT( !transaction.downloadQueue.isEmpty() ); @@ -280,7 +288,7 @@ void MerginApi::downloadNextItem( const QString &projectFullName ) QUrl url( mApiRoot + QStringLiteral( "/v1/project/raw/" ) + projectFullName ); QUrlQuery query; - // Handles special chars in a filePath (e.g prevents to convert "+" sign into a space) + // Handles special chars in a filePath (e.g. prevents to convert "+" sign in to a space) query.addQueryItem( "file", item.filePath.toUtf8().toPercentEncoding() ); query.addQueryItem( "version", QStringLiteral( "v%1" ).arg( item.version ) ); if ( item.downloadDiff ) @@ -289,8 +297,8 @@ void MerginApi::downloadNextItem( const QString &projectFullName ) QNetworkRequest request = getDefaultRequest(); request.setUrl( url ); - request.setAttribute( static_cast( AttrProjectFullName ), projectFullName ); request.setAttribute( static_cast( AttrTempFileName ), item.tempFileName ); + request.setAttribute( static_cast( AttrProjectId ), projectId ); QString range; if ( item.rangeFrom != -1 && item.rangeTo != -1 ) @@ -300,7 +308,7 @@ void MerginApi::downloadNextItem( const QString &projectFullName ) } QNetworkReply *reply = mManager->get( request ); - connect( reply, &QNetworkReply::finished, this, [this, item]() { downloadItemReplyFinished( item ); } ); + connect( reply, &QNetworkReply::finished, this, [this, item] { downloadItemReplyFinished( item ); } ); transaction.replyPullItems.insert( reply ); @@ -308,21 +316,21 @@ void MerginApi::downloadNextItem( const QString &projectFullName ) ( !range.isEmpty() ? " Range: " + range : QString() ) ); } -void MerginApi::removeProjectsTempFolder( const QString &projectNamespace, const QString &projectName ) +void MerginApi::removeProjectsTempFolder( const QString &projectNamespace, const QString &projectName ) const { if ( projectNamespace.isEmpty() || projectName.isEmpty() ) - return; // otherwise we could remove enitre users temp or entire .temp + return; // otherwise we could remove entire users temp or entire .temp - QString path = getTempProjectDir( getFullProjectName( projectNamespace, projectName ) ); + const QString path = getTempProjectDir( CoreUtils::getFullProjectName( projectNamespace, projectName ) ); QDir( path ).removeRecursively(); } -QNetworkRequest MerginApi::getDefaultRequest( bool withAuth ) +QNetworkRequest MerginApi::getDefaultRequest( const bool withAuth ) const { QNetworkRequest request; - QString info = CoreUtils::appInfo(); + const QString info = CoreUtils::appInfo(); request.setRawHeader( "User-Agent", QByteArray( info.toUtf8() ) ); - QString deviceId = CoreUtils::deviceUuid(); + const QString deviceId = CoreUtils::deviceUuid(); request.setRawHeader( "X-Device-Id", QByteArray( deviceId.toUtf8() ) ); if ( withAuth ) { @@ -334,13 +342,13 @@ QNetworkRequest MerginApi::getDefaultRequest( bool withAuth ) bool MerginApi::projectFileHasBeenUpdated( const ProjectDiff &diff ) { - for ( QString filePath : diff.remoteAdded ) + for ( const QString &filePath : diff.remoteAdded ) { if ( CoreUtils::hasProjectFileExtension( filePath ) ) return true; } - for ( QString filePath : diff.remoteUpdated ) + for ( const QString &filePath : diff.remoteUpdated ) { if ( CoreUtils::hasProjectFileExtension( filePath ) ) return true; @@ -354,7 +362,7 @@ bool MerginApi::supportsSelectiveSync() const return mSupportsSelectiveSync; } -void MerginApi::setSupportsSelectiveSync( bool supportsSelectiveSync ) +void MerginApi::setSupportsSelectiveSync( const bool supportsSelectiveSync ) { mSupportsSelectiveSync = supportsSelectiveSync; } @@ -364,7 +372,7 @@ bool MerginApi::apiSupportsSubscriptions() const return mApiSupportsSubscriptions; } -void MerginApi::setApiSupportsSubscriptions( bool apiSupportsSubscriptions ) +void MerginApi::setApiSupportsSubscriptions( const bool apiSupportsSubscriptions ) { if ( mApiSupportsSubscriptions != apiSupportsSubscriptions ) { @@ -389,22 +397,24 @@ QString MerginApi::getApiKey( const QString &serverName ) return "not-secret-key"; } -void MerginApi::downloadItemReplyFinished( DownloadQueueItem item ) +void MerginApi::downloadItemReplyFinished( const DownloadQueueItem &item ) { QNetworkReply *r = qobject_cast( sender() ); Q_ASSERT( r ); - QString projectFullName = r->request().attribute( static_cast( AttrProjectFullName ) ).toString(); - QString tempFileName = r->request().attribute( static_cast( AttrTempFileName ) ).toString(); - Q_ASSERT( mTransactionalStatus.contains( projectFullName ) ); - TransactionStatus &transaction = mTransactionalStatus[projectFullName]; + const QString tempFileName = r->request().attribute( static_cast( AttrTempFileName ) ).toString(); + const QString projectId = r->request().attribute( static_cast( AttrProjectId ) ).toString(); + Q_ASSERT( mTransactionalStatus.contains( projectId ) ); + TransactionStatus &transaction = mTransactionalStatus[projectId]; Q_ASSERT( transaction.replyPullItems.contains( r ) ); + const MerginProjectMetadata project = MerginProjectMetadata::fromJson( transaction.projectMetadata ); + const QString projectFullName = CoreUtils::getFullProjectName( project.projectNamespace, project.name ); if ( r->error() == QNetworkReply::NoError ) { - QByteArray data = r->readAll(); + const QByteArray data = r->readAll(); CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Downloaded item (%1 bytes)" ).arg( data.size() ) ); - QString tempFolder = getTempProjectDir( projectFullName ); - QString tempFilePath = tempFolder + "/" + tempFileName; + const QString tempFolder = getTempProjectDir( projectFullName ); + const QString tempFilePath = tempFolder + "/" + tempFileName; createPathIfNotExists( tempFilePath ); // save to a tmp file, assemble at the end QFile file( tempFilePath ); @@ -418,7 +428,7 @@ void MerginApi::downloadItemReplyFinished( DownloadQueueItem item ) CoreUtils::log( "pull " + projectFullName, "Failed to open for writing: " + file.fileName() ); } transaction.transferedSize += data.size(); - emit syncProjectStatusChanged( projectFullName, transaction.transferedSize / transaction.totalSize ); + emit syncProjectStatusChanged( projectId, static_cast( transaction.transferedSize ) / transaction.totalSize ); transaction.replyPullItems.remove( r ); r->deleteLater(); @@ -426,30 +436,29 @@ void MerginApi::downloadItemReplyFinished( DownloadQueueItem item ) if ( !transaction.downloadQueue.isEmpty() ) { // one request finished, let's start another one - downloadNextItem( projectFullName ); + downloadNextItem( projectId ); } - else if ( transaction.replyPullItems.isEmpty() ) { // nothing else to download and all requests are finished, we're done - finalizeProjectPull( projectFullName ); + finalizeProjectPull( projectId ); } else { // no more requests to start, but there are pending requests - let's do nothing and wait } } - else if ( transaction.retryCount < transaction.MAX_RETRY_COUNT && isRetryableNetworkError( r ) ) + else if ( transaction.retryCount < TransactionStatus::MAX_RETRY_COUNT && isRetryableNetworkError( r ) ) { transaction.retryCount++; transaction.downloadQueue.append( item ); - CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Retrying download (attempt %1 of %2)" ).arg( transaction.retryCount ) - .arg( transaction.MAX_RETRY_COUNT ) ); + CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Retrying download (attempt %1 of %2)" ) + .arg( transaction.retryCount ).arg( TransactionStatus::MAX_RETRY_COUNT ) ); - downloadNextItem( projectFullName ); + downloadNextItem( projectId ); - emit downloadItemRetried( projectFullName, transaction.retryCount ); + emit downloadItemRetried( projectId, transaction.retryCount ); transaction.replyPullItems.remove( r ); r->deleteLater(); } @@ -469,10 +478,10 @@ void MerginApi::downloadItemReplyFinished( DownloadQueueItem item ) if ( !transaction.pullItemsAborting ) { // the first failed request will abort all the other pending requests too, and finish pull with error - abortPullItems( projectFullName ); + abortPullItems( projectId ); // signal a networking error - we may retry - int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); - emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: downloadFile" ), httpCode, projectFullName ); + const int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); + emit networkErrorOccurred( serverMsg, httpCode, projectId ); } else { @@ -481,10 +490,12 @@ void MerginApi::downloadItemReplyFinished( DownloadQueueItem item ) } } -void MerginApi::abortPullItems( const QString &projectFullName ) +void MerginApi::abortPullItems( const QString &projectId ) { - Q_ASSERT( mTransactionalStatus.contains( projectFullName ) ); - TransactionStatus &transaction = mTransactionalStatus[projectFullName]; + Q_ASSERT( mTransactionalStatus.contains( projectId ) ); + TransactionStatus &transaction = mTransactionalStatus[projectId]; + const MerginProjectMetadata project = MerginProjectMetadata::fromJson( transaction.projectMetadata ); + const QString projectFullName = CoreUtils::getFullProjectName( project.projectNamespace, project.name ); transaction.pullItemsAborting = true; @@ -501,7 +512,7 @@ void MerginApi::abortPullItems( const QString &projectFullName ) QDir( transaction.projectDir ).removeRecursively(); } - finishProjectSync( projectFullName, false ); + finishProjectSync( projectFullName, projectId, false ); } void MerginApi::cacheServerConfig() @@ -509,15 +520,18 @@ void MerginApi::cacheServerConfig() QNetworkReply *r = qobject_cast( sender() ); Q_ASSERT( r ); - QString projectFullName = r->request().attribute( static_cast( AttrProjectFullName ) ).toString(); + const QString projectId = r->request().attribute( static_cast( AttrProjectId ) ).toString(); - Q_ASSERT( mTransactionalStatus.contains( projectFullName ) ); - TransactionStatus &transaction = mTransactionalStatus[projectFullName]; + Q_ASSERT( mTransactionalStatus.contains( projectId ) ); + TransactionStatus &transaction = mTransactionalStatus[projectId]; Q_ASSERT( r == transaction.replyPullServerConfig ); + const MerginProjectMetadata project = MerginProjectMetadata::fromJson( transaction.projectMetadata ); + const QString projectFullName = CoreUtils::getFullProjectName( project.projectNamespace, project.name ); + if ( r->error() == QNetworkReply::NoError ) { - QByteArray data = r->readAll(); + const QByteArray data = r->readAll(); CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Downloaded mergin config (%1 bytes)" ).arg( data.size() ) ); transaction.config = MerginConfig::fromJson( data ); @@ -525,7 +539,7 @@ void MerginApi::cacheServerConfig() transaction.replyPullServerConfig->deleteLater(); transaction.replyPullServerConfig = nullptr; - prepareDownloadConfig( projectFullName, true ); + prepareDownloadConfig( projectId, true ); } else { @@ -547,23 +561,23 @@ void MerginApi::cacheServerConfig() CoreUtils::removeDir( transaction.projectDir ); } - int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); - emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: downloadFile" ), httpCode, projectFullName ); + const int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); + emit networkErrorOccurred( serverMsg, httpCode, projectId ); - finishProjectSync( projectFullName, false ); + finishProjectSync( projectFullName, projectId, false ); } } -void MerginApi::pushFile( const QString &projectFullName, const QString &transactionUUID, MerginFile file, int chunkNo ) +void MerginApi::pushFile( const QString &projectFullName, const QString &projectId, const QString &transactionUUID, const MerginFile &file, const int chunkNo ) { if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK ) { return; } - Q_ASSERT( mTransactionalStatus.contains( projectFullName ) ); - TransactionStatus &transaction = mTransactionalStatus[projectFullName]; + Q_ASSERT( mTransactionalStatus.contains( projectId ) ); + TransactionStatus &transaction = mTransactionalStatus[projectId]; QString chunkID = file.chunks.at( chunkNo ); @@ -583,10 +597,11 @@ void MerginApi::pushFile( const QString &projectFullName, const QString &transac } QNetworkRequest request = getDefaultRequest(); - QUrl url( mApiRoot + QStringLiteral( "/v1/project/push/chunk/%1/%2" ).arg( transactionUUID, chunkID ) ); + const QUrl url( mApiRoot + QStringLiteral( "/v1/project/push/chunk/%1/%2" ).arg( transactionUUID, chunkID ) ); request.setUrl( url ); request.setRawHeader( "Content-Type", "application/octet-stream" ); request.setAttribute( static_cast( AttrProjectFullName ), projectFullName ); + request.setAttribute( static_cast( AttrProjectId ), projectId ); Q_ASSERT( !transaction.replyPushFile ); transaction.replyPushFile = mManager->post( request, data ); @@ -595,21 +610,22 @@ void MerginApi::pushFile( const QString &projectFullName, const QString &transac CoreUtils::log( "push " + projectFullName, QStringLiteral( "Uploading item: " ) + url.toString() ); } -void MerginApi::pushStart( const QString &projectFullName, const QByteArray &json ) +void MerginApi::pushStart( const QString &projectFullName, const QString &projectId, const QByteArray &json ) { if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK ) { return; } - Q_ASSERT( mTransactionalStatus.contains( projectFullName ) ); - TransactionStatus &transaction = mTransactionalStatus[projectFullName]; + Q_ASSERT( mTransactionalStatus.contains( projectId ) ); + TransactionStatus &transaction = mTransactionalStatus[projectId]; QNetworkRequest request = getDefaultRequest(); - QUrl url( mApiRoot + QStringLiteral( "/v1/project/push/%1" ).arg( projectFullName ) ); + const QUrl url( mApiRoot + QStringLiteral( "/v1/project/push/%1" ).arg( projectFullName ) ); request.setUrl( url ); request.setRawHeader( "Content-Type", "application/json" ); request.setAttribute( static_cast( AttrProjectFullName ), projectFullName ); + request.setAttribute( static_cast( AttrProjectId ), projectId ); Q_ASSERT( !transaction.replyPushStart ); transaction.replyPushStart = mManager->post( request, json ); @@ -618,19 +634,20 @@ void MerginApi::pushStart( const QString &projectFullName, const QByteArray &jso CoreUtils::log( "push " + projectFullName, QStringLiteral( "Starting push request: " ) + url.toString() ); } -void MerginApi::cancelPush( const QString &projectFullName ) +void MerginApi::cancelPush( const QString &projectId ) { if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK ) { return; } - if ( !mTransactionalStatus.contains( projectFullName ) ) + if ( !mTransactionalStatus.contains( projectId ) ) return; - CoreUtils::log( "push " + projectFullName, QStringLiteral( "User requested cancel" ) ); + const TransactionStatus &transaction = mTransactionalStatus[projectId]; + const QString projectFullName = mLocalProjects.projectFromProjectId( projectId ).fullName(); - TransactionStatus &transaction = mTransactionalStatus[projectFullName]; + CoreUtils::log( "push " + projectFullName, QStringLiteral( "User requested cancel" ) ); // There is an open transaction, abort it followed by calling cancelUpload again. if ( transaction.replyPushProjectInfo ) @@ -645,20 +662,20 @@ void MerginApi::cancelPush( const QString &projectFullName ) } else if ( transaction.replyPushFile ) { - QString transactionUUID = transaction.transactionUUID; // copy transaction uuid as the transaction object will be gone after abort + const QString transactionUUID = transaction.transactionUUID; // copy transaction uuid as the transaction object will be gone after abort CoreUtils::log( "push " + projectFullName, QStringLiteral( "Aborting upload file" ) ); transaction.replyPushFile->abort(); // will trigger pushFileReplyFinished slot and emit sync finished // also need to cancel the transaction - sendPushCancelRequest( projectFullName, transactionUUID ); + sendPushCancelRequest( projectFullName, projectId, transactionUUID ); } else if ( transaction.replyPushFinish ) { - QString transactionUUID = transaction.transactionUUID; // copy transaction uuid as the transaction object will be gone after abort + const QString transactionUUID = transaction.transactionUUID; // copy transaction uuid as the transaction object will be gone after abort CoreUtils::log( "push " + projectFullName, QStringLiteral( "Aborting upload finish" ) ); transaction.replyPushFinish->abort(); // will trigger pushFinishReplyFinished slot and emit sync finished - sendPushCancelRequest( projectFullName, transactionUUID ); + sendPushCancelRequest( projectFullName, projectId, transactionUUID ); } else { @@ -667,27 +684,28 @@ void MerginApi::cancelPush( const QString &projectFullName ) } -void MerginApi::sendPushCancelRequest( const QString &projectFullName, const QString &transactionUUID ) +void MerginApi::sendPushCancelRequest( const QString &projectFullName, const QString &projectId, const QString &transactionUUID ) { QNetworkRequest request = getDefaultRequest(); - QUrl url( mApiRoot + QStringLiteral( "/v1/project/push/cancel/%1" ).arg( transactionUUID ) ); + const QUrl url( mApiRoot + QStringLiteral( "/v1/project/push/cancel/%1" ).arg( transactionUUID ) ); request.setUrl( url ); request.setRawHeader( "Content-Type", "application/json" ); - request.setAttribute( static_cast( AttrProjectFullName ), projectFullName ); + request.setAttribute( static_cast( AttrProjectId ), projectId ); - QNetworkReply *reply = mManager->post( request, QByteArray() ); + const QNetworkReply *reply = mManager->post( request, QByteArray() ); connect( reply, &QNetworkReply::finished, this, &MerginApi::pushCancelReplyFinished ); CoreUtils::log( "push " + projectFullName, QStringLiteral( "Requesting upload transaction cancel: " ) + url.toString() ); } -void MerginApi::cancelPull( const QString &projectFullName ) +void MerginApi::cancelPull( const QString &projectId ) { - if ( !mTransactionalStatus.contains( projectFullName ) ) + if ( !mTransactionalStatus.contains( projectId ) ) return; + const QString projectFullName = mLocalProjects.projectFromProjectId( projectId ).fullName(); CoreUtils::log( "pull " + projectFullName, QStringLiteral( "User requested cancel" ) ); - TransactionStatus &transaction = mTransactionalStatus[projectFullName]; + const TransactionStatus &transaction = mTransactionalStatus[projectId]; if ( transaction.replyPullProjectInfo ) { @@ -704,7 +722,7 @@ void MerginApi::cancelPull( const QString &projectFullName ) else if ( !transaction.replyPullItems.isEmpty() ) { // we're already downloading some files - abortPullItems( projectFullName ); + abortPullItems( projectId ); } else { @@ -712,21 +730,22 @@ void MerginApi::cancelPull( const QString &projectFullName ) } } -void MerginApi::pushFinish( const QString &projectFullName, const QString &transactionUUID ) +void MerginApi::pushFinish( const QString &projectFullName, const QString &projectId, const QString &transactionUUID ) { if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK ) { return; } - Q_ASSERT( mTransactionalStatus.contains( projectFullName ) ); - TransactionStatus &transaction = mTransactionalStatus[projectFullName]; + Q_ASSERT( mTransactionalStatus.contains( projectId ) ); + TransactionStatus &transaction = mTransactionalStatus[projectId]; QNetworkRequest request = getDefaultRequest(); - QUrl url( mApiRoot + QStringLiteral( "/v1/project/push/finish/%1" ).arg( transactionUUID ) ); + const QUrl url( mApiRoot + QStringLiteral( "/v1/project/push/finish/%1" ).arg( transactionUUID ) ); request.setUrl( url ); request.setRawHeader( "Content-Type", "application/json" ); request.setAttribute( static_cast( AttrProjectFullName ), projectFullName ); + request.setAttribute( static_cast( AttrProjectId ), projectId ); Q_ASSERT( !transaction.replyPushFinish ); transaction.replyPushFinish = mManager->post( request, QByteArray() ); @@ -735,25 +754,24 @@ void MerginApi::pushFinish( const QString &projectFullName, const QString &trans CoreUtils::log( "push " + projectFullName, QStringLiteral( "Requesting transaction finish: " ) + transactionUUID ); } -bool MerginApi::pullProject( const QString &projectNamespace, const QString &projectName, bool withAuth ) +bool MerginApi::pullProject( const QString &projectFullName, const QString &projectId, const bool withAuth ) { - QString projectFullName = getFullProjectName( projectNamespace, projectName ); bool pullHasStarted = false; CoreUtils::log( "pull " + projectFullName, "### Starting ###" ); - QNetworkReply *reply = getProjectInfo( projectFullName, withAuth ); + QNetworkReply *reply = getProjectInfo( projectFullName, projectId, withAuth ); if ( reply ) { CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Requesting project info: " ) + reply->request().url().toString() ); - Q_ASSERT( !mTransactionalStatus.contains( projectFullName ) ); - mTransactionalStatus.insert( projectFullName, TransactionStatus() ); - mTransactionalStatus[projectFullName].replyPullProjectInfo = reply; - mTransactionalStatus[projectFullName].configAllowed = mSupportsSelectiveSync; - mTransactionalStatus[projectFullName].type = TransactionStatus::Pull; + Q_ASSERT( !mTransactionalStatus.contains( projectId ) ); + mTransactionalStatus.insert( projectId, TransactionStatus() ); + mTransactionalStatus[projectId].replyPullProjectInfo = reply; + mTransactionalStatus[projectId].configAllowed = mSupportsSelectiveSync; + mTransactionalStatus[projectId].type = TransactionStatus::Pull; - emit syncProjectStatusChanged( projectFullName, 0 ); + emit syncProjectStatusChanged( projectId, 0 ); connect( reply, &QNetworkReply::finished, this, &MerginApi::pullInfoReplyFinished ); pullHasStarted = true; @@ -766,27 +784,26 @@ bool MerginApi::pullProject( const QString &projectNamespace, const QString &pro return pullHasStarted; } -bool MerginApi::pushProject( const QString &projectNamespace, const QString &projectName, bool isInitialPush ) +bool MerginApi::pushProject( const QString &projectFullName, const QString &projectId, const bool isInitialPush ) { - QString projectFullName = getFullProjectName( projectNamespace, projectName ); bool pushHasStarted = false; CoreUtils::log( "push " + projectFullName, "### Starting ###" ); - QNetworkReply *reply = getProjectInfo( projectFullName ); + QNetworkReply *reply = getProjectInfo( projectFullName, projectId ); if ( reply ) { CoreUtils::log( "push " + projectFullName, QStringLiteral( "Requesting project info: " ) + reply->request().url().toString() ); // create entry about pending upload for the project - Q_ASSERT( !mTransactionalStatus.contains( projectFullName ) ); - mTransactionalStatus.insert( projectFullName, TransactionStatus() ); - mTransactionalStatus[projectFullName].replyPushProjectInfo = reply; - mTransactionalStatus[projectFullName].isInitialPush = isInitialPush; - mTransactionalStatus[projectFullName].configAllowed = mSupportsSelectiveSync; - mTransactionalStatus[projectFullName].type = TransactionStatus::Push; + Q_ASSERT( !mTransactionalStatus.contains( projectId ) ); + mTransactionalStatus.insert( projectId, TransactionStatus() ); + mTransactionalStatus[projectId].replyPushProjectInfo = reply; + mTransactionalStatus[projectId].isInitialPush = isInitialPush; + mTransactionalStatus[projectId].configAllowed = mSupportsSelectiveSync; + mTransactionalStatus[projectId].type = TransactionStatus::Push; - emit syncProjectStatusChanged( projectFullName, 0 ); + emit syncProjectStatusChanged( projectId, 0 ); connect( reply, &QNetworkReply::finished, this, &MerginApi::pushInfoReplyFinished ); pushHasStarted = true; @@ -813,8 +830,8 @@ void MerginApi::authorize( const QString &login, const QString &password ) mUserAuth->blockSignals( false ); QNetworkRequest request = getDefaultRequest( false ); - QString urlString = mApiRoot + QStringLiteral( "/v1/auth/login" ); - QUrl url( urlString ); + const QString urlString = mApiRoot + QStringLiteral( "/v1/auth/login" ); + const QUrl url( urlString ); request.setUrl( url ); request.setRawHeader( "Content-Type", "application/json" ); @@ -823,9 +840,9 @@ void MerginApi::authorize( const QString &login, const QString &password ) jsonObject.insert( QStringLiteral( "login" ), login ); jsonObject.insert( QStringLiteral( "password" ), mUserAuth->password() ); jsonDoc.setObject( jsonObject ); - QByteArray json = jsonDoc.toJson( QJsonDocument::Compact ); + const QByteArray json = jsonDoc.toJson( QJsonDocument::Compact ); - QNetworkReply *reply = mManager->post( request, json ); + const QNetworkReply *reply = mManager->post( request, json ); connect( reply, &QNetworkReply::finished, this, &MerginApi::authorizeFinished ); CoreUtils::log( "auth", QStringLiteral( "Requesting authorization: " ) + url.toString() ); } @@ -952,7 +969,7 @@ void MerginApi::ssoConnectionsReplyFinished() void MerginApi::registerUser( const QString &email, const QString &password, - bool acceptedTOC ) + const bool acceptedTOC ) { // Some very basic checks, so we do not validate everything if ( !CoreUtils::isValidEmail( email ) ) @@ -978,15 +995,15 @@ void MerginApi::registerUser( const QString &email, if ( !acceptedTOC ) { - QString msg = tr( "Please accept Terms and Privacy Policy" ); + const QString msg = tr( "Please accept Terms and Privacy Policy" ); emit registrationFailed( msg, RegistrationError::RegistrationErrorType::TOC ); return; } // request QNetworkRequest request = getDefaultRequest( false ); - QString urlString = mApiRoot + QStringLiteral( "/v1/auth/register" ); - QUrl url( urlString ); + const QString urlString = mApiRoot + QStringLiteral( "/v1/auth/register" ); + const QUrl url( urlString ); request.setUrl( url ); request.setRawHeader( "Content-Type", "application/json" ); @@ -996,18 +1013,18 @@ void MerginApi::registerUser( const QString &email, jsonObject.insert( QStringLiteral( "password" ), password ); jsonObject.insert( QStringLiteral( "api_key" ), getApiKey( mApiRoot ) ); jsonDoc.setObject( jsonObject ); - QByteArray json = jsonDoc.toJson( QJsonDocument::Compact ); - QNetworkReply *reply = mManager->post( request, json ); - connect( reply, &QNetworkReply::finished, this, [ = ]() { this->registrationFinished( email, password ); } ); + const QByteArray json = jsonDoc.toJson( QJsonDocument::Compact ); + const QNetworkReply *reply = mManager->post( request, json ); + connect( reply, &QNetworkReply::finished, this, [ = ] { this->registrationFinished( email, password ); } ); CoreUtils::log( "auth", QStringLiteral( "Requesting registration: " ) + url.toString() ); } -void MerginApi::postRegisterUser( const QString &marketingChannel, const QString &industry, bool wantsNewsletter ) +void MerginApi::postRegisterUser( const QString &marketingChannel, const QString &industry, const bool wantsNewsletter ) { // Some very basic checks, so we do not validate everything if ( marketingChannel.isEmpty() || industry.isEmpty() ) { - QString msg = tr( "Marketing source cannot be empty" ); + const QString msg = tr( "Marketing source cannot be empty" ); emit postRegistrationFailed( msg ); return; } @@ -1015,14 +1032,14 @@ void MerginApi::postRegisterUser( const QString &marketingChannel, const QString // Some very basic checks, so we do not validate everything if ( industry.isEmpty() ) { - QString msg = tr( "Industry cannot be empty" ); + const QString msg = tr( "Industry cannot be empty" ); emit postRegistrationFailed( msg ); return; } // request QNetworkRequest request = getDefaultRequest( false ); - QString urlString = mApiRoot + QStringLiteral( "/v1/post-register" ); - QUrl url( urlString ); + const QString urlString = mApiRoot + QStringLiteral( "/v1/post-register" ); + const QUrl url( urlString ); request.setUrl( url ); request.setRawHeader( "Content-Type", "application/json" ); @@ -1032,9 +1049,9 @@ void MerginApi::postRegisterUser( const QString &marketingChannel, const QString jsonObject.insert( QStringLiteral( "marketing_channel" ), marketingChannel ); jsonObject.insert( QStringLiteral( "subscribe" ), wantsNewsletter ); jsonDoc.setObject( jsonObject ); - QByteArray json = jsonDoc.toJson( QJsonDocument::Compact ); - QNetworkReply *reply = mManager->post( request, json ); - connect( reply, &QNetworkReply::finished, this, [ = ]() { this->postRegistrationFinished(); } ); + const QByteArray json = jsonDoc.toJson( QJsonDocument::Compact ); + const QNetworkReply *reply = mManager->post( request, json ); + connect( reply, &QNetworkReply::finished, this, [ = ] { this->postRegistrationFinished(); } ); CoreUtils::log( "auth", QStringLiteral( "Requesting post-registration: " ) + url.toString() ); } @@ -1061,10 +1078,10 @@ void MerginApi::getUserInfo() } QNetworkRequest request = getDefaultRequest( true ); - QUrl url( urlString ); + const QUrl url( urlString ); request.setUrl( url ); - QNetworkReply *reply = mManager->get( request ); + const QNetworkReply *reply = mManager->get( request ); CoreUtils::log( "user info", QStringLiteral( "Requesting user info: " ) + url.toString() ); connect( reply, &QNetworkReply::finished, this, &MerginApi::getUserInfoFinished ); } @@ -1086,12 +1103,12 @@ void MerginApi::getWorkspaceInfo() return; } - QString urlString = mApiRoot + QStringLiteral( "/v1/workspace/%1" ).arg( mUserInfo->activeWorkspaceId() ); + const QString urlString = mApiRoot + QStringLiteral( "/v1/workspace/%1" ).arg( mUserInfo->activeWorkspaceId() ); QNetworkRequest request = getDefaultRequest(); - QUrl url( urlString ); + const QUrl url( urlString ); request.setUrl( url ); - QNetworkReply *reply = mManager->get( request ); + const QNetworkReply *reply = mManager->get( request ); CoreUtils::log( "workspace info", QStringLiteral( "Requesting workspace info: " ) + url.toString() ); connect( reply, &QNetworkReply::finished, this, &MerginApi::getWorkspaceInfoReplyFinished ); } @@ -1125,10 +1142,10 @@ void MerginApi::getServiceInfo() } QNetworkRequest request = getDefaultRequest( true ); - QUrl url( urlString ); + const QUrl url( urlString ); request.setUrl( url ); - QNetworkReply *reply = mManager->get( request ); + const QNetworkReply *reply = mManager->get( request ); connect( reply, &QNetworkReply::finished, this, &MerginApi::getServiceInfoReplyFinished ); @@ -1144,22 +1161,22 @@ void MerginApi::getServiceInfoReplyFinished() { CoreUtils::log( "Service info", QStringLiteral( "Success" ) ); - QJsonDocument doc = QJsonDocument::fromJson( r->readAll() ); + const QJsonDocument doc = QJsonDocument::fromJson( r->readAll() ); if ( doc.isObject() ) { - QJsonObject docObj = doc.object(); + const QJsonObject docObj = doc.object(); mSubscriptionInfo->setFromJson( docObj ); } } else { QString serverMsg = extractServerErrorMsg( r->readAll() ); - QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "getServiceInfo" ), r->errorString(), serverMsg ); + const QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "getServiceInfo" ), r->errorString(), serverMsg ); CoreUtils::log( "Service info", QStringLiteral( "FAILED - %1" ).arg( message ) ); mSubscriptionInfo->clear(); - int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); + const int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); if ( httpCode == 404 ) { // no such API on the server, do not emit anything @@ -1170,7 +1187,7 @@ void MerginApi::getServiceInfoReplyFinished() } else { - emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: getServiceInfo" ) ); + emit networkErrorOccurred( serverMsg ); } } @@ -1204,21 +1221,21 @@ void MerginApi::clearAuth() CoreUtils::log( QStringLiteral( "Auth" ), QStringLiteral( "Cleared auth and user data cache" ) ); } -QString MerginApi::resetPasswordUrl() +QString MerginApi::resetPasswordUrl() const { if ( !mApiRoot.isEmpty() ) { - QUrl base( mApiRoot ); + const QUrl base( mApiRoot ); return base.resolved( QUrl( "login/reset" ) ).toString(); } - return QString(); + return {}; } -bool MerginApi::createProject( const QString &projectNamespace, const QString &projectName, bool isPublic ) +bool MerginApi::createProject( const QString &projectNamespace, const QString &projectName, const QString &projectId, const bool isPublic ) { if ( !validateAuth() ) { - emit missingAuthorizationError( projectName ); + emit missingAuthorizationError( projectId ); return false; } @@ -1227,48 +1244,47 @@ bool MerginApi::createProject( const QString &projectNamespace, const QString &p return false; } - QString projectFullName = getFullProjectName( projectNamespace, projectName ); - QNetworkRequest request = getDefaultRequest(); - QUrl url( mApiRoot + QString( "/v1/project/%1" ).arg( projectNamespace ) ); + const QUrl url( mApiRoot + QString( "/v1/project/%1" ).arg( projectNamespace ) ); request.setUrl( url ); request.setRawHeader( "Content-Type", "application/json" ); request.setRawHeader( "Accept", "application/json" ); - request.setAttribute( static_cast( AttrProjectFullName ), projectFullName ); + request.setAttribute( static_cast( AttrProjectId ), projectId ); + request.setAttribute( static_cast( AttrProjectFullName ), + CoreUtils::getFullProjectName( projectNamespace, projectName ) ); QJsonDocument jsonDoc; QJsonObject jsonObject; jsonObject.insert( QStringLiteral( "name" ), projectName ); jsonObject.insert( QStringLiteral( "public" ), isPublic ); jsonDoc.setObject( jsonObject ); - QByteArray json = jsonDoc.toJson( QJsonDocument::Compact ); + const QByteArray json = jsonDoc.toJson( QJsonDocument::Compact ); - QNetworkReply *reply = mManager->post( request, json ); + const QNetworkReply *reply = mManager->post( request, json ); connect( reply, &QNetworkReply::finished, this, &MerginApi::createProjectFinished ); - CoreUtils::log( "create " + projectFullName, QStringLiteral( "Requesting project creation: " ) + url.toString() ); + CoreUtils::log( "create " + CoreUtils::getFullProjectName( projectNamespace, projectName ), + QStringLiteral( "Requesting project creation: " ) + url.toString() ); return true; } -void MerginApi::deleteProject( const QString &projectNamespace, const QString &projectName, bool informUser ) +void MerginApi::deleteProject( const QString &projectId, bool informUser ) { if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK ) { return; } - QString projectFullName = getFullProjectName( projectNamespace, projectName ); - QNetworkRequest request = getDefaultRequest(); - QUrl url( mApiRoot + QStringLiteral( "/v1/project/%1" ).arg( projectFullName ) ); + const QUrl url( mApiRoot + QStringLiteral( "/v2/projects/%1" ).arg( projectId ) ); request.setUrl( url ); - request.setAttribute( static_cast( AttrProjectFullName ), projectFullName ); - QNetworkReply *reply = mManager->deleteResource( request ); - connect( reply, &QNetworkReply::finished, this, [this, informUser]() { this->deleteProjectFinished( informUser );} ); - CoreUtils::log( "delete " + projectFullName, QStringLiteral( "Requesting project deletion: " ) + url.toString() ); + request.setAttribute( static_cast( AttrProjectId ), projectId ); + const QNetworkReply *reply = mManager->deleteResource( request ); + connect( reply, &QNetworkReply::finished, this, [this, informUser] { this->deleteProjectFinished( informUser );} ); + CoreUtils::log( "delete " + projectId, QStringLiteral( "Requesting immediate project deletion: " ) + url.toString() ); } -void MerginApi::saveAuthData() +void MerginApi::saveAuthData() const { QSettings settings; settings.beginGroup( "Input/" ); @@ -1283,51 +1299,74 @@ void MerginApi::createProjectFinished() QNetworkReply *r = qobject_cast( sender() ); Q_ASSERT( r ); - QString projectFullName = r->request().attribute( static_cast( AttrProjectFullName ) ).toString(); - - QString projectNamespace, projectName; - extractProjectName( projectFullName, projectNamespace, projectName ); + const QString projectId = r->request().attribute( static_cast( AttrProjectId ) ).toString(); + const QString projectFullName = r->request().attribute( static_cast( AttrProjectFullName ) ).toString(); if ( r->error() == QNetworkReply::NoError ) { CoreUtils::log( "create " + projectFullName, QStringLiteral( "Success" ) ); - emit projectCreated( projectFullName, true ); - // Upload data if createProject has been called for a local project with empty namespace (case of migrating a project) - for ( const LocalProject &info : mLocalProjects.projects() ) + if ( mLocalProjects.projects().contains( projectId ) ) { - if ( info.projectName == projectName && info.projectNamespace.isEmpty() ) + // we shoot this signal for tests + emit projectCreated( projectId, true ); + // we remove the process saved under the old ID and pushProject will insert new process with new ID + emit projectCreated( projectId, false ); + + const QNetworkReply *reply = getProjectInfo( projectFullName, projectId ); + connect( reply, &QNetworkReply::finished, this, [ this, projectId, projectFullName ] { - mLocalProjects.updateNamespace( info.projectDir, projectNamespace ); - emit projectAttachedToMergin( projectFullName, projectName ); + QNetworkReply *reply = qobject_cast( sender() ); + Q_ASSERT( reply ); - QDir projectDir( info.projectDir ); - if ( projectDir.exists() && !projectDir.isEmpty() ) + if ( reply->error() == QNetworkReply::NoError ) { - pushProject( projectNamespace, projectName, true ); + const QByteArray data = reply->readAll(); + const MerginProjectMetadata serverProject = MerginProjectMetadata::fromJson( data ); + + mLocalProjects.updateProjectId( projectId, serverProject.projectId ); + mLocalProjects.updateNamespace( serverProject.projectId, serverProject.projectNamespace ); + emit projectAttachedToMergin( serverProject.projectId ); + + const LocalProject info = mLocalProjects.projectFromProjectId( serverProject.projectId ); + const QDir projectDir( info.projectDir ); + if ( projectDir.exists() && !projectDir.isEmpty() ) + { + pushProject( projectFullName, serverProject.projectId, true ); + } } - } + else + { + CoreUtils::log( "create " + projectFullName, QString( "Failed to get new ID for project %1" ).arg( projectFullName ) ); + } + + reply->deleteLater(); + } ); + } + else + { + emit projectCreated( projectId, true ); } } else { - QByteArray data = r->readAll(); - QString code = extractServerErrorCode( data ); + const QByteArray data = r->readAll(); + const QString code = extractServerErrorCode( data ); QString serverMsg = extractServerErrorMsg( data ); - QString message = QStringLiteral( "FAILED - %1: %2" ).arg( r->errorString(), serverMsg ); - int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); - bool showLimitReachedDialog = EnumHelper::isEqual( code, ErrorCode::ProjectsLimitHit ); - bool userMissingPermissions = ( httpCode == 403 ); + const QString message = QStringLiteral( "FAILED - %1: %2" ).arg( r->errorString(), serverMsg ); + const int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); + const bool showLimitReachedDialog = EnumHelper::isEqual( code, ErrorCode::ProjectsLimitHit ); + const bool userMissingPermissions = ( httpCode == 403 ); CoreUtils::log( "create " + projectFullName, message ); - emit projectCreated( projectFullName, false ); + emit projectCreated( projectId, false ); if ( showLimitReachedDialog ) { int maxProjects = 0; - QVariant maxProjectVariant = extractServerErrorValue( data, "projects_quota" ); + const QVariant maxProjectVariant = extractServerErrorValue( data, "projects_quota" ); if ( maxProjectVariant.isValid() ) maxProjects = maxProjectVariant.toInt(); emit projectLimitReached( maxProjects, serverMsg ); @@ -1341,35 +1380,34 @@ void MerginApi::createProjectFinished() { emit notifyError( tr( "Couldn't create the project. Please try again later or contact support if the problem persists." ) ); emit projectCreationFailed(); - emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: createProject" ), httpCode, projectName ); } } r->deleteLater(); } -void MerginApi::deleteProjectFinished( bool informUser ) +void MerginApi::deleteProjectFinished( const bool informUser ) { QNetworkReply *r = qobject_cast( sender() ); Q_ASSERT( r ); - QString projectFullName = r->request().attribute( static_cast( AttrProjectFullName ) ).toString(); + const QString projectId = r->request().attribute( static_cast( AttrProjectId ) ).toString(); if ( r->error() == QNetworkReply::NoError ) { - CoreUtils::log( "delete " + projectFullName, QStringLiteral( "Success" ) ); + CoreUtils::log( "delete " + projectId, QStringLiteral( "Success" ) ); if ( informUser ) emit notifySuccess( QStringLiteral( "Project deleted" ) ); - emit serverProjectDeleted( projectFullName, true ); } else { - QString serverMsg = extractServerErrorMsg( r->readAll() ); - CoreUtils::log( "delete " + projectFullName, QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) ); - emit serverProjectDeleted( projectFullName, false ); - emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: deleteProject" ) ); + const QString serverMsg = extractServerErrorMsg( r->readAll() ); + const int serverErrorCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); + CoreUtils::log( "delete " + projectId, QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) ); + emit networkErrorOccurred( serverMsg, serverErrorCode ); } + r->deleteLater(); } @@ -1382,10 +1420,10 @@ void MerginApi::authorizeFinished() { CoreUtils::log( "auth", QStringLiteral( "Success" ) ); const QByteArray data = r->readAll(); - QJsonDocument doc = QJsonDocument::fromJson( data ); + const QJsonDocument doc = QJsonDocument::fromJson( data ); if ( doc.isObject() ) { - QJsonObject docObj = doc.object(); + const QJsonObject docObj = doc.object(); mUserAuth->setFromJson( docObj ); } else @@ -1402,8 +1440,8 @@ void MerginApi::authorizeFinished() else { QString serverMsg = extractServerErrorMsg( r->readAll() ); - QVariant statusCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ); - int status = statusCode.toInt(); + const QVariant statusCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ); + const int status = statusCode.toInt(); CoreUtils::log( "Auth", QStringLiteral( "FAILED - %1. %2 (%3)" ).arg( r->errorString(), serverMsg, QString::number( status ) ) ); if ( status == 401 ) @@ -1425,7 +1463,7 @@ void MerginApi::authorizeFinished() // keep login and password // this is problem with internet connection or server // so do not force user to input login credentials again - emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: authorize" ) ); + emit networkErrorOccurred( serverMsg ); } // in case of any error, just clean token and request new one @@ -1447,7 +1485,7 @@ void MerginApi::registrationFinished( const QString &login, const QString &passw if ( r->error() == QNetworkReply::NoError ) { CoreUtils::log( "register", QStringLiteral( "Success" ) ); - QString msg = tr( "Registration successful" ); + const QString msg = tr( "Registration successful" ); emit notifySuccess( msg ); if ( !login.isEmpty() && !password.isEmpty() ) // log in immediately @@ -1506,7 +1544,7 @@ void MerginApi::registrationFinished( const QString &login, const QString &passw { const QString msg = QStringLiteral( "Mergin API error: register" ); emit registrationFailed( msg, RegistrationError::RegistrationErrorType::OTHER ); - emit networkErrorOccurred( serverMsg, msg ); + emit networkErrorOccurred( serverMsg ); } } r->deleteLater(); @@ -1526,9 +1564,7 @@ void MerginApi::postRegistrationFinished() { QString serverMsg = extractServerErrorMsg( r->readAll() ); CoreUtils::log( "post-register", QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) ); - QVariant statusCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ); - int status = statusCode.toInt(); - emit postRegistrationFailed( QStringLiteral( "Post-registation failed %1" ).arg( serverMsg ) ); + emit postRegistrationFailed( QStringLiteral( "Post-registration failed %1" ).arg( serverMsg ) ); } emit notifySuccess( tr( "Workspace created" ) ); r->deleteLater(); @@ -1545,10 +1581,10 @@ void MerginApi::pingMerginReplyFinished() if ( r->error() == QNetworkReply::NoError ) { CoreUtils::log( "ping", QStringLiteral( "Success" ) ); - QJsonDocument doc = QJsonDocument::fromJson( r->readAll() ); + const QJsonDocument doc = QJsonDocument::fromJson( r->readAll() ); if ( doc.isObject() ) { - QJsonObject obj = doc.object(); + const QJsonObject obj = doc.object(); apiVersion = obj.value( QStringLiteral( "version" ) ).toString(); serverSupportsSubscriptions = obj.value( QStringLiteral( "subscriptions_enabled" ) ).toBool(); } @@ -1577,11 +1613,11 @@ void MerginApi::onPlanProductIdChanged() } } -QNetworkReply *MerginApi::getProjectInfo( const QString &projectFullName, bool withAuth ) +QNetworkReply *MerginApi::getProjectInfo( const QString &projectFullName, const QString &projectId, const bool withAuth ) { if ( withAuth && !validateAuth() ) { - emit missingAuthorizationError( projectFullName ); + emit missingAuthorizationError( projectId ); return nullptr; } @@ -1591,7 +1627,7 @@ QNetworkReply *MerginApi::getProjectInfo( const QString &projectFullName, bool w } int sinceVersion = -1; - LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); + const LocalProject projectInfo = mLocalProjects.projectFromProjectId( projectId ); if ( projectInfo.isValid() ) { // let's also fetch the recent history of diffable files @@ -1603,12 +1639,68 @@ QNetworkReply *MerginApi::getProjectInfo( const QString &projectFullName, bool w if ( sinceVersion != -1 ) query.addQueryItem( QStringLiteral( "since" ), QStringLiteral( "v%1" ).arg( sinceVersion ) ); - QUrl url( mApiRoot + QStringLiteral( "/v1/project/%1" ).arg( projectFullName ) ); + QUrl url{}; + url.setUrl( mApiRoot + QStringLiteral( "/v1/project/%1" ).arg( projectFullName ) ); url.setQuery( query ); QNetworkRequest request = getDefaultRequest( withAuth ); request.setUrl( url ); + request.setAttribute( static_cast( AttrProjectId ), projectId ); request.setAttribute( static_cast( AttrProjectFullName ), projectFullName ); + request.setAttribute( static_cast( AttrAuthUsed ), withAuth ); + + return mManager->get( request ); +} + +QNetworkReply *MerginApi::getProjectDetails( const QString &projectId, const bool withAuth ) +{ + if ( withAuth && !validateAuth() ) + { + emit missingAuthorizationError( projectId ); + return nullptr; + } + + if ( mApiVersionStatus != MerginApiStatus::OK ) + { + return nullptr; + } + + QUrl url{}; + url.setUrl( mApiRoot + QStringLiteral( "/v1/project/by_uuid/%1" ).arg( projectId ) ); + + QNetworkRequest request = getDefaultRequest( withAuth ); + request.setUrl( url ); + request.setAttribute( static_cast( AttrProjectId ), projectId ); + + return mManager->get( request ); +} + +QNetworkReply *MerginApi::getProjectsDetails( const QStringList &projectIds, const bool withAuth ) +{ + if ( withAuth && !validateAuth() ) + { + for ( const QString &projectId : projectIds ) + { + emit missingAuthorizationError( projectId ); + } + return nullptr; + } + + if ( mApiVersionStatus != MerginApiStatus::OK ) + { + return nullptr; + } + + QUrlQuery query; + query.addQueryItem( QStringLiteral( "uuids" ), projectIds.join( "," ) ); + + QUrl url{}; + url.setUrl( mApiRoot + QStringLiteral( "/v1/project/by_uuids" ) ); + url.setQuery( query ); + + QNetworkRequest request = getDefaultRequest( withAuth ); + request.setUrl( url ); + request.setAttribute( static_cast( AttrAuthUsed ), withAuth ); return mManager->get( request ); } @@ -1640,7 +1732,7 @@ bool MerginApi::validateAuth() return true; } -void MerginApi::checkMerginVersion( QString apiVersion, bool serverSupportsSubscriptions, QString msg ) +void MerginApi::checkMerginVersion( const QString &apiVersion, const bool serverSupportsSubscriptions, const QString &msg ) { setApiSupportsSubscriptions( serverSupportsSubscriptions ); @@ -1649,7 +1741,7 @@ void MerginApi::checkMerginVersion( QString apiVersion, bool serverSupportsSubsc int major = -1; int minor = -1; - bool validVersion = parseVersion( apiVersion, major, minor ); + const bool validVersion = parseVersion( apiVersion, major, minor ); if ( !validVersion ) { @@ -1657,7 +1749,7 @@ void MerginApi::checkMerginVersion( QString apiVersion, bool serverSupportsSubsc return; } - if ( ( MERGIN_API_VERSION_MAJOR == major && MERGIN_API_VERSION_MINOR <= minor ) || ( MERGIN_API_VERSION_MAJOR < major ) ) + if ( ( MERGIN_API_VERSION_MAJOR == major && MERGIN_API_VERSION_MINOR <= minor ) || MERGIN_API_VERSION_MAJOR < major ) { setApiVersionStatus( MerginApiStatus::OK ); } @@ -1672,56 +1764,40 @@ void MerginApi::checkMerginVersion( QString apiVersion, bool serverSupportsSubsc } } -bool MerginApi::extractProjectName( const QString &sourceString, QString &projectNamespace, QString &name ) -{ - QStringList parts = sourceString.split( "/" ); - if ( parts.length() > 1 ) - { - projectNamespace = parts.at( parts.length() - 2 ); - name = parts.last(); - return true; - } - else - { - name = sourceString; - return false; - } -} - QString MerginApi::extractServerErrorCode( const QByteArray &data ) { - QVariant code = extractServerErrorValue( data, QStringLiteral( "code" ) ); + const QVariant code = extractServerErrorValue( data, QStringLiteral( "code" ) ); if ( code.isValid() ) return code.toString(); - return QString(); + return {}; } QVariant MerginApi::extractServerErrorValue( const QByteArray &data, const QString &key ) { - QJsonDocument doc = QJsonDocument::fromJson( data ); + const QJsonDocument doc = QJsonDocument::fromJson( data ); if ( doc.isObject() ) { - QJsonObject obj = doc.object(); + const QJsonObject obj = doc.object(); if ( obj.contains( key ) ) { - QJsonValue val = obj.value( key ); + const QJsonValue val = obj.value( key ); return val.toVariant(); } } - return QVariant(); + return {}; } QString MerginApi::extractServerErrorMsg( const QByteArray &data ) { QString serverMsg = "[can't parse server error]"; - QJsonDocument doc = QJsonDocument::fromJson( data ); + const QJsonDocument doc = QJsonDocument::fromJson( data ); if ( doc.isObject() ) { - QJsonObject obj = doc.object(); + const QJsonObject obj = doc.object(); if ( obj.contains( QStringLiteral( "detail" ) ) ) { - QJsonValue vDetail = obj.value( "detail" ); + const QJsonValue vDetail = obj.value( "detail" ); if ( vDetail.isString() ) { serverMsg = vDetail.toString(); @@ -1733,10 +1809,10 @@ QString MerginApi::extractServerErrorMsg( const QByteArray &data ) } else if ( obj.contains( QStringLiteral( "name" ) ) ) { - QJsonValue val = obj.value( "name" ); + const QJsonValue val = obj.value( "name" ); if ( val.isArray() ) { - QJsonArray errors = val.toArray(); + const QJsonArray errors = val.toArray(); QStringList messages; for ( auto it = errors.constBegin(); it != errors.constEnd(); ++it ) { @@ -1760,17 +1836,17 @@ QString MerginApi::extractServerErrorMsg( const QByteArray &data ) } -LocalProject MerginApi::getLocalProject( const QString &projectFullName ) +LocalProject MerginApi::getLocalProject( const QString &projectId ) const { - return mLocalProjects.projectFromMerginName( projectFullName ); + return mLocalProjects.projectFromProjectId( projectId ); } ProjectDiff MerginApi::localProjectChanges( const QString &projectDir ) { - MerginProjectMetadata projectMetadata = MerginProjectMetadata::fromCachedJson( projectDir + "/" + sMetadataFile ); - QList localFiles = getLocalProjectFiles( projectDir + "/" ); + const MerginProjectMetadata projectMetadata = MerginProjectMetadata::fromCachedJson( projectDir + "/" + sMetadataFile ); + const QList localFiles = getLocalProjectFiles( projectDir + "/" ); - MerginConfig config = MerginConfig::fromFile( projectDir + "/" + sMerginConfigFile ); + const MerginConfig config = MerginConfig::fromFile( projectDir + "/" + sMerginConfigFile ); return compareProjectFiles( projectMetadata.files, projectMetadata.files, localFiles, projectDir, config.isValid, config ); } @@ -1795,10 +1871,10 @@ bool MerginApi::parseVersion( const QString &version, int &major, int &minor ) return true; } -bool MerginApi::hasLocalProjectChanges( const QString &projectDir, bool supportsSelectiveSync ) +bool MerginApi::hasLocalProjectChanges( const QString &projectDir, const bool supportsSelectiveSync ) { - MerginProjectMetadata projectMetadata = MerginProjectMetadata::fromCachedJson( projectDir + "/" + sMetadataFile ); - QList localFiles = getLocalProjectFiles( projectDir + "/" ); + const MerginProjectMetadata projectMetadata = MerginProjectMetadata::fromCachedJson( projectDir + "/" + sMetadataFile ); + const QList localFiles = getLocalProjectFiles( projectDir + "/" ); MerginConfig config; if ( supportsSelectiveSync ) @@ -1809,16 +1885,11 @@ bool MerginApi::hasLocalProjectChanges( const QString &projectDir, bool supports return hasLocalChanges( projectMetadata.files, localFiles, projectDir, config ); } -QString MerginApi::getTempProjectDir( const QString &projectFullName ) +QString MerginApi::getTempProjectDir( const QString &projectFullName ) const { return mDataDir + "/" + TEMP_FOLDER + projectFullName; } -QString MerginApi::getFullProjectName( QString projectNamespace, QString projectName ) // TODO: move to inpututils? -{ - return QString( "%1/%2" ).arg( projectNamespace ).arg( projectName ); -} - MerginApiStatus::VersionStatus MerginApi::apiVersionStatus() const { return mApiVersionStatus; @@ -1840,32 +1911,18 @@ void MerginApi::pingMergin() setApiVersionStatus( MerginApiStatus::PENDING ); QNetworkRequest request = getDefaultRequest( false ); - QUrl url( mApiRoot + QStringLiteral( "/ping" ) ); + const QUrl url( mApiRoot + QStringLiteral( "/ping" ) ); request.setUrl( url ); - QNetworkReply *reply = mManager->get( request ); + const QNetworkReply *reply = mManager->get( request ); CoreUtils::log( "ping", QStringLiteral( "Requesting: " ) + url.toString() ); connect( reply, &QNetworkReply::finished, this, &MerginApi::pingMerginReplyFinished ); } -void MerginApi::migrateProjectToMergin( const QString &projectName, const QString &projectNamespace ) -{ - CoreUtils::log( "migrate project", projectName ); - if ( projectNamespace.isEmpty() ) - { - createProject( mUserInfo->username(), projectName ); - } - else - { - createProject( projectNamespace, projectName ); - } -} - -void MerginApi::detachProjectFromMergin( const QString &projectNamespace, const QString &projectName, bool informUser ) +void MerginApi::detachProjectFromMergin( const QString &projectId, const bool informUser ) { // Remove mergin folder - QString projectFullName = getFullProjectName( projectNamespace, projectName ); - LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); + const LocalProject projectInfo = mLocalProjects.projectFromProjectId( projectId ); if ( projectInfo.isValid() ) { @@ -1879,7 +1936,7 @@ void MerginApi::detachProjectFromMergin( const QString &projectNamespace, const if ( informUser ) emit notifySuccess( tr( "Project detached from the server" ) ); - emit projectDetached( projectFullName ); + emit projectDetached( projectId ); } QString MerginApi::apiRoot() const @@ -1925,7 +1982,7 @@ QList MerginApi::getLocalProjectFiles( const QString &projectPath ) ProjectChecksumCache checksumCache( projectPath ); QSet localFiles = listFiles( projectPath ); - for ( QString p : localFiles ) + for ( const QString &p : localFiles ) { MerginFile file; file.checksum = checksumCache.get( p ); @@ -1936,7 +1993,7 @@ QList MerginApi::getLocalProjectFiles( const QString &projectPath ) merginFiles.append( file ); } - qint64 elapsed = timer.elapsed(); + const qint64 elapsed = timer.elapsed(); if ( elapsed > 100 ) { 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 ) ); @@ -1944,7 +2001,7 @@ QList MerginApi::getLocalProjectFiles( const QString &projectPath ) return merginFiles; } -void MerginApi::listProjectsReplyFinished( QString requestId ) +void MerginApi::listProjectsReplyFinished( const QString &requestId ) { QNetworkReply *r = qobject_cast( sender() ); Q_ASSERT( r ); @@ -1955,11 +2012,11 @@ void MerginApi::listProjectsReplyFinished( QString requestId ) if ( r->error() == QNetworkReply::NoError ) { - QUrlQuery query( r->request().url().query() ); + const QUrlQuery query( r->request().url().query() ); requestedPage = query.queryItemValue( "page" ).toInt(); - QByteArray data = r->readAll(); - QJsonDocument doc = QJsonDocument::fromJson( data ); + const QByteArray data = r->readAll(); + const QJsonDocument doc = QJsonDocument::fromJson( data ); if ( doc.isObject() ) { @@ -1972,8 +2029,8 @@ void MerginApi::listProjectsReplyFinished( QString requestId ) else { QString serverMsg = extractServerErrorMsg( r->readAll() ); - QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listProjects" ), r->errorString(), serverMsg ); - emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: listProjects" ) ); + const QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listProjects" ), r->errorString(), serverMsg ); + emit networkErrorOccurred( serverMsg ); CoreUtils::log( "list projects", QStringLiteral( "FAILED - %1" ).arg( message ) ); emit listProjectsFailed(); @@ -1984,7 +2041,7 @@ void MerginApi::listProjectsReplyFinished( QString requestId ) emit listProjectsFinished( projectList, projectCount, requestedPage, requestId ); } -void MerginApi::listProjectsByNameReplyFinished( QString requestId ) +void MerginApi::listProjectsByNameReplyFinished( const QString &requestId ) { QNetworkReply *r = qobject_cast( sender() ); Q_ASSERT( r ); @@ -1993,16 +2050,16 @@ void MerginApi::listProjectsByNameReplyFinished( QString requestId ) if ( r->error() == QNetworkReply::NoError ) { - QByteArray data = r->readAll(); - QJsonDocument json = QJsonDocument::fromJson( data ); + const QByteArray data = r->readAll(); + const QJsonDocument json = QJsonDocument::fromJson( data ); projectList = parseProjectsFromJson( json ); CoreUtils::log( "list projects by name", QStringLiteral( "Success - got %1 projects" ).arg( projectList.count() ) ); } else { QString serverMsg = extractServerErrorMsg( r->readAll() ); - QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listProjectsByName" ), r->errorString(), serverMsg ); - emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: listProjectsByName" ) ); + const QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listProjectsByName" ), r->errorString(), serverMsg ); + emit networkErrorOccurred( serverMsg ); CoreUtils::log( "list projects by name", QStringLiteral( "FAILED - %1" ).arg( message ) ); emit listProjectsFailed(); @@ -2013,12 +2070,73 @@ void MerginApi::listProjectsByNameReplyFinished( QString requestId ) emit listProjectsByNameFinished( projectList, requestId ); } +void MerginApi::getProjectsDetailsReplyFinished() +{ + QNetworkReply *r = qobject_cast( sender() ); + Q_ASSERT( r ); + + if ( r->error() == QNetworkReply::NoError ) + { + const QJsonDocument doc = QJsonDocument::fromJson( r->readAll() ); + if ( doc.isObject() ) + { + const QJsonObject response = doc.object(); + for ( const QString key : response.keys() ) + { + // if the selected project ID is in pending transactions we need to restart the whole process as this call + // was a fallback + if ( mTransactionalStatus.contains( key ) ) + { + QJsonObject project = response.value( key ).toObject(); + QString projectFullName = QString( "%1/%2" ).arg( project.value( "namespace" ).toString(), project.value( "name" ).toString() ); + const bool withAuth = r->request().attribute( static_cast( AttrAuthUsed ) ).toBool(); + if ( mTransactionalStatus[key].type == TransactionStatus::Pull ) + { + pullProject( projectFullName, key, withAuth ); + } + else + { + pushProject( projectFullName, key, withAuth ); + } + } + } + } + } +} + +void MerginApi::refetchBrokenProjectsReplyFinished() +{ + QNetworkReply *r = qobject_cast( sender() ); + Q_ASSERT( r ); + + MerginProjectsList projectList; + + if ( r->error() == QNetworkReply::NoError ) + { + const QByteArray data = r->readAll(); + const QJsonDocument json = QJsonDocument::fromJson( data ); + projectList = parseProjectsFromJson( json ); + CoreUtils::log( "refetch broken projects", QStringLiteral( "Success - got %1 projects" ).arg( projectList.count() ) ); + } + else + { + QString serverMsg = extractServerErrorMsg( r->readAll() ); + const QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "refetchBrokenProjects" ), r->errorString(), serverMsg ); + emit networkErrorOccurred( serverMsg ); + CoreUtils::log( "refetch broken projects", QStringLiteral( "FAILED - %1" ).arg( message ) ); + } + + r->deleteLater(); + + emit refetchBrokenProjectsFinished( projectList ); +} + void MerginApi::finalizeProjectPullCopy( const QString &projectFullName, const QString &projectDir, const QString &tempDir, const QString &filePath, const QList &items ) { CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Copying new content of " ) + filePath ); - QString dest = projectDir + "/" + filePath; + const QString dest = projectDir + "/" + filePath; createPathIfNotExists( dest ); QFile f( dest ); @@ -2042,25 +2160,25 @@ void MerginApi::finalizeProjectPullCopy( const QString &projectFullName, const Q f.close(); - // if diffable, copy to .mergin dir so we have a basefile - if ( MerginApi::isFileDiffable( filePath ) ) + // if diffable, copy to .mergin dir so we have a base file + if ( isFileDiffable( filePath ) ) { - QString basefile = projectDir + "/.mergin/" + filePath; - createPathIfNotExists( basefile ); + const QString baseFile = projectDir + "/.mergin/" + filePath; + createPathIfNotExists( baseFile ); - if ( !QFile::remove( basefile ) ) + if ( !QFile::remove( baseFile ) ) { - CoreUtils::log( "pull " + projectFullName, "failed to remove old basefile for: " + filePath ); + CoreUtils::log( "pull " + projectFullName, "failed to remove old base file for: " + filePath ); } - if ( !QFile::copy( dest, basefile ) ) + if ( !QFile::copy( dest, baseFile ) ) { - CoreUtils::log( "pull " + projectFullName, "failed to copy new basefile for: " + filePath ); + CoreUtils::log( "pull " + projectFullName, "failed to copy new base file for: " + filePath ); } } } -bool MerginApi::finalizeProjectPullApplyDiff( const QString &projectFullName, const QString &projectDir, const QString &tempDir, const QString &filePath, const QList &items ) +bool MerginApi::finalizeProjectPullApplyDiff( const QString &projectFullName, const QString &projectId, const QString &projectDir, const QString &tempDir, const QString &filePath, const QList &items ) { CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Applying diff to " ) + filePath ); @@ -2070,28 +2188,28 @@ bool MerginApi::finalizeProjectPullApplyDiff( const QString &projectFullName, co QString src = tempDir + "/" + CoreUtils::uuidWithoutBraces( QUuid::createUuid() ); QString dest = projectDir + "/" + filePath; - QString basefile = projectDir + "/.mergin/" + filePath; + QString baseFile = projectDir + "/.mergin/" + filePath; - LocalProject info = mLocalProjects.projectFromMerginName( projectFullName ); + LocalProject info = mLocalProjects.projectFromProjectId( projectId ); // add conflict files to project dir so they can be synced - QString conflictfile = CoreUtils::findUniquePath( CoreUtils::generateEditConflictFileName( dest, mUserInfo->username(), info.localVersion ) ); + QString conflictFile = CoreUtils::findUniquePath( CoreUtils::generateEditConflictFileName( dest, mUserInfo->username(), info.localVersion ) ); createPathIfNotExists( src ); createPathIfNotExists( dest ); - createPathIfNotExists( basefile ); + createPathIfNotExists( baseFile ); QStringList diffFiles; for ( const auto &item : items ) diffFiles << tempDir + "/" + item.tempFileName; // - // let's first assemble server's file from our basefile + diffs + // let's first assemble server's file from our base file + diffs // - if ( !QFile::copy( basefile, src ) ) + if ( !QFile::copy( baseFile, src ) ) { - CoreUtils::log( "pull " + projectFullName, "assemble server file fail: copying failed " + basefile + " to " + src ); + CoreUtils::log( "pull " + projectFullName, "assemble server file fail: copying failed " + baseFile + " to " + src ); // TODO: this is a critical failure - we should abort pull } @@ -2101,7 +2219,7 @@ bool MerginApi::finalizeProjectPullApplyDiff( const QString &projectFullName, co CoreUtils::log( "pull " + projectFullName, "server file assembly failed: " + filePath ); // TODO: this is a critical failure - we should abort pull - // TODO: we could try to delete the basefile and re-download it from scratch on next sync + // TODO: we could try to delete the base file and re-download it from scratch on next sync } else { @@ -2113,10 +2231,10 @@ bool MerginApi::finalizeProjectPullApplyDiff( const QString &projectFullName, co // bool hasConflicts = false; - bool res = GeodiffUtils::rebase( basefile, + bool res = GeodiffUtils::rebase( baseFile, src, dest, - conflictfile + conflictFile ); if ( res ) { @@ -2129,8 +2247,8 @@ bool MerginApi::finalizeProjectPullApplyDiff( const QString &projectFullName, co // not good... something went wrong in rebase - we need to save the local changes // let's put them into a conflict file and use the server version hasConflicts = true; - LocalProject info = mLocalProjects.projectFromMerginName( projectFullName ); - QString newDest = CoreUtils::findUniquePath( CoreUtils::generateConflictedCopyFileName( dest, mUserInfo->username(), info.localVersion ) ); + LocalProject localProject = mLocalProjects.projectFromProjectId( projectId ); + QString newDest = CoreUtils::findUniquePath( CoreUtils::generateConflictedCopyFileName( dest, mUserInfo->username(), localProject.localVersion ) ); if ( !QFile::rename( dest, newDest ) ) { CoreUtils::log( "pull " + projectFullName, "failed rename of conflicting file after failed geodiff rebase: " + filePath ); @@ -2142,31 +2260,33 @@ bool MerginApi::finalizeProjectPullApplyDiff( const QString &projectFullName, co } // - // finally update our basefile + // finally update our base file // - if ( !QFile::remove( basefile ) ) + if ( !QFile::remove( baseFile ) ) { - CoreUtils::log( "pull " + projectFullName, "failed removal of old basefile: " + filePath ); + CoreUtils::log( "pull " + projectFullName, "failed removal of old base file: " + filePath ); // TODO: this is a critical failure - we should abort pull } - if ( !QFile::rename( src, basefile ) ) + if ( !QFile::rename( src, baseFile ) ) { - CoreUtils::log( "pull " + projectFullName, "failed rename of basefile using new server content: " + filePath ); + CoreUtils::log( "pull " + projectFullName, "failed rename of base file using new server content: " + filePath ); // TODO: this is a critical failure - we should abort pull } return hasConflicts; } -void MerginApi::finalizeProjectPull( const QString &projectFullName ) +void MerginApi::finalizeProjectPull( const QString &projectId ) { - Q_ASSERT( mTransactionalStatus.contains( projectFullName ) ); - TransactionStatus &transaction = mTransactionalStatus[projectFullName]; + Q_ASSERT( mTransactionalStatus.contains( projectId ) ); + TransactionStatus &transaction = mTransactionalStatus[projectId]; + const MerginProjectMetadata project = MerginProjectMetadata::fromJson( transaction.projectMetadata ); + const QString projectFullName = CoreUtils::getFullProjectName( project.projectNamespace, project.name ); - QString projectDir = transaction.projectDir; - QString tempProjectDir = getTempProjectDir( projectFullName ); + const QString projectDir = transaction.projectDir; + const QString tempProjectDir = getTempProjectDir( projectFullName ); CoreUtils::log( "pull " + projectFullName, "Running update tasks" ); @@ -2184,7 +2304,7 @@ void MerginApi::finalizeProjectPull( const QString &projectFullName ) { // move local file to conflict file QString origPath = projectDir + "/" + finalizationItem.filePath; - LocalProject info = mLocalProjects.projectFromMerginName( projectFullName ); + const LocalProject info = mLocalProjects.projectFromProjectId( projectId ); QString newPath = CoreUtils::findUniquePath( CoreUtils::generateConflictedCopyFileName( origPath, mUserInfo->username(), info.localVersion ) ); if ( !QFile::rename( origPath, newPath ) ) { @@ -2202,7 +2322,7 @@ void MerginApi::finalizeProjectPull( const QString &projectFullName ) { // applying diff can result in conflicted copy too, in this case // we need to update gpkgSchemaChanged flag. - bool res = finalizeProjectPullApplyDiff( projectFullName, projectDir, tempProjectDir, finalizationItem.filePath, finalizationItem.data ); + const bool res = finalizeProjectPullApplyDiff( projectFullName, projectId, projectDir, tempProjectDir, finalizationItem.filePath, finalizationItem.data ); transaction.gpkgSchemaChanged = res; break; } @@ -2225,7 +2345,7 @@ void MerginApi::finalizeProjectPull( const QString &projectFullName ) } // check there are no files left - int tmpFilesLeft = QDir( tempProjectDir ).entryList( QDir::NoDotAndDotDot ).count(); + const int tmpFilesLeft = static_cast( QDir( tempProjectDir ).entryList( QDir::NoDotAndDotDot ).count() ); if ( tmpFilesLeft ) { CoreUtils::log( "pull " + projectFullName, "Some temporary files were left - this should not happen..." ); @@ -2234,19 +2354,16 @@ void MerginApi::finalizeProjectPull( const QString &projectFullName ) QDir( tempProjectDir ).removeRecursively(); // add the local project if not there yet - if ( !mLocalProjects.projectFromMerginName( projectFullName ).isValid() ) + if ( !mLocalProjects.projectFromProjectId( projectId ).isValid() ) { - QString projectNamespace, projectName; - extractProjectName( projectFullName, projectNamespace, projectName ); - // remove download in progress file if ( !QFile::remove( CoreUtils::downloadInProgressFilePath( transaction.projectDir ) ) ) - CoreUtils::log( QStringLiteral( "sync %1" ).arg( projectFullName ), QStringLiteral( "Failed to remove download in progress file for project name %1" ).arg( projectName ) ); + CoreUtils::log( QStringLiteral( "sync %1" ).arg( projectFullName ), QStringLiteral( "Failed to remove download in progress file for project name %1" ).arg( project.name ) ); - mLocalProjects.addMerginProject( projectDir, projectNamespace, projectName ); + mLocalProjects.addMerginProject( projectDir, project.projectNamespace, project.name, projectId ); } - finishProjectSync( projectFullName, true ); + finishProjectSync( projectFullName, projectId, true ); } @@ -2256,9 +2373,10 @@ void MerginApi::pushStartReplyFinished() Q_ASSERT( r ); QString projectFullName = r->request().attribute( static_cast( AttrProjectFullName ) ).toString(); + QString projectId = r->request().attribute( static_cast( AttrProjectId ) ).toString(); - Q_ASSERT( mTransactionalStatus.contains( projectFullName ) ); - TransactionStatus &transaction = mTransactionalStatus[projectFullName]; + Q_ASSERT( mTransactionalStatus.contains( projectId ) ); + TransactionStatus &transaction = mTransactionalStatus[projectId]; Q_ASSERT( r == transaction.replyPushStart ); if ( r->error() == QNetworkReply::NoError ) @@ -2283,26 +2401,26 @@ void MerginApi::pushStartReplyFinished() if ( transaction.transactionUUID.isEmpty() ) { CoreUtils::log( "push " + projectFullName, QStringLiteral( "Fail! Could not acquire transaction ID" ) ); - finishProjectSync( projectFullName, false ); + finishProjectSync( projectFullName, projectId, false ); } CoreUtils::log( "push " + projectFullName, QStringLiteral( "Push request accepted. Transaction ID: " ) + transactionUUID ); - MerginFile file = files.first(); - pushFile( projectFullName, transactionUUID, file ); + const MerginFile &file = files.first(); + pushFile( projectFullName, projectId, transactionUUID, file ); emit pushFilesStarted(); } else // pushing only files to be removed { // we are done here - no upload of chunks, no request to "finish" - // because server immediatelly creates a new version without starting a transaction to upload chunks + // because server immediately creates a new version without starting a transaction to upload chunks CoreUtils::log( "push " + projectFullName, QStringLiteral( "Push request accepted and no files to upload" ) ); transaction.projectMetadata = data; transaction.version = MerginProjectMetadata::fromJson( data ).version; - finishProjectSync( projectFullName, true ); + finishProjectSync( projectFullName, projectId, true ); } } else @@ -2326,26 +2444,23 @@ void MerginApi::pushStartReplyFinished() qreal uploadSize = 0; for ( const MerginFile &f : files ) { - uploadSize += f.size; + uploadSize += static_cast( f.size ); } emit storageLimitReached( uploadSize ); // remove project if it was first time sync - migration if ( transaction.isInitialPush ) { - QString projectNamespace, projectName; - extractProjectName( projectFullName, projectNamespace, projectName ); - - detachProjectFromMergin( projectNamespace, projectName, false ); - deleteProject( projectNamespace, projectName, false ); + detachProjectFromMergin( projectId, false ); + deleteProject( projectId, false ); } } else { int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); - emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: pushStartReply" ), httpCode, projectFullName ); + emit networkErrorOccurred( serverMsg, httpCode, projectId ); } - finishProjectSync( projectFullName, false ); + finishProjectSync( projectFullName, projectId, false ); } } @@ -2355,14 +2470,15 @@ void MerginApi::pushFileReplyFinished() Q_ASSERT( r ); QString projectFullName = r->request().attribute( static_cast( AttrProjectFullName ) ).toString(); + QString projectId = r->request().attribute( static_cast( AttrProjectId ) ).toString(); - Q_ASSERT( mTransactionalStatus.contains( projectFullName ) ); - TransactionStatus &transaction = mTransactionalStatus[projectFullName]; + Q_ASSERT( mTransactionalStatus.contains( projectId ) ); + TransactionStatus &transaction = mTransactionalStatus[projectId]; Q_ASSERT( r == transaction.replyPushFile ); - QStringList params = ( r->url().toString().split( "/" ) ); - QString transactionUUID = params.at( params.length() - 2 ); - QString chunkID = params.at( params.length() - 1 ); + QStringList params = r->url().toString().split( "/" ); + const QString &transactionUUID = params.at( params.length() - 2 ); + const QString &chunkID = params.at( params.length() - 1 ); Q_ASSERT( transactionUUID == transaction.transactionUUID ); if ( r->error() == QNetworkReply::NoError ) @@ -2373,26 +2489,26 @@ void MerginApi::pushFileReplyFinished() transaction.replyPushFile = nullptr; MerginFile currentFile = transaction.pushQueue.first(); - int chunkNo = currentFile.chunks.indexOf( chunkID ); + int chunkNo = static_cast( currentFile.chunks.indexOf( chunkID ) ); if ( chunkNo < currentFile.chunks.size() - 1 ) { - pushFile( projectFullName, transactionUUID, currentFile, chunkNo + 1 ); + pushFile( projectFullName, projectId, transactionUUID, currentFile, chunkNo + 1 ); } else { transaction.transferedSize += currentFile.size; - emit syncProjectStatusChanged( projectFullName, transaction.transferedSize / transaction.totalSize ); + emit syncProjectStatusChanged( projectId, static_cast( transaction.transferedSize ) / transaction.totalSize ); transaction.pushQueue.removeFirst(); if ( !transaction.pushQueue.isEmpty() ) { MerginFile nextFile = transaction.pushQueue.first(); - pushFile( projectFullName, transactionUUID, nextFile ); + pushFile( projectFullName, projectId, transactionUUID, nextFile ); } else { - pushFinish( projectFullName, transactionUUID ); + pushFinish( projectFullName, projectId, transactionUUID ); } } } @@ -2405,12 +2521,12 @@ void MerginApi::pushFileReplyFinished() CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1. %2" ).arg( r->errorString(), serverMsg ) ); int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); - emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: pushFile" ), httpCode, projectFullName ); + emit networkErrorOccurred( serverMsg, httpCode, projectId ); transaction.replyPushFile->deleteLater(); transaction.replyPushFile = nullptr; - finishProjectSync( projectFullName, false ); + finishProjectSync( projectFullName, projectId, false ); } } @@ -2419,52 +2535,67 @@ void MerginApi::pullInfoReplyFinished() QNetworkReply *r = qobject_cast( sender() ); Q_ASSERT( r ); - QString projectFullName = r->request().attribute( static_cast( AttrProjectFullName ) ).toString(); + const QString projectFullName = r->request().attribute( static_cast( AttrProjectFullName ) ).toString(); + const QString projectId = r->request().attribute( static_cast( AttrProjectId ) ).toString(); - Q_ASSERT( mTransactionalStatus.contains( projectFullName ) ); - TransactionStatus &transaction = mTransactionalStatus[projectFullName]; + Q_ASSERT( mTransactionalStatus.contains( projectId ) ); + TransactionStatus &transaction = mTransactionalStatus[projectId]; Q_ASSERT( r == transaction.replyPullProjectInfo ); if ( r->error() == QNetworkReply::NoError ) { - QByteArray data = r->readAll(); + const QByteArray data = r->readAll(); CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Downloaded project info." ) ); transaction.replyPullProjectInfo->deleteLater(); transaction.replyPullProjectInfo = nullptr; - prepareProjectPull( projectFullName, data ); + prepareProjectPull( projectId, data ); } else { - QString serverMsg = extractServerErrorMsg( r->readAll() ); + // if the operation was cancelled we finish pull, but if something failed we try fetching project info by project ID if ( r->error() == QNetworkReply::OperationCanceledError ) - serverMsg = sSyncCanceledMessage; + { + const QString serverMsg = sSyncCanceledMessage; + const QString message = QStringLiteral( "Network API error: %1(): %2" ).arg( QStringLiteral( "projectInfo" ), r->errorString() ); + CoreUtils::log( "pull " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) ); - QString message = QStringLiteral( "Network API error: %1(): %2" ).arg( QStringLiteral( "projectInfo" ), r->errorString() ); - CoreUtils::log( "pull " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) ); + const int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); + emit networkErrorOccurred( serverMsg, httpCode, projectId ); - int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); - emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: pullInfo" ), httpCode, projectFullName ); + transaction.replyPullProjectInfo->deleteLater(); + transaction.replyPullProjectInfo = nullptr; + + finishProjectSync( projectFullName, projectId, false ); + } + else + { + const QString serverMsg = extractServerErrorMsg( r->readAll() ); + const QString message = QStringLiteral( "Network API error: %1(): %2" ).arg( QStringLiteral( "projectInfo" ), r->errorString() ); + CoreUtils::log( "pull " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) ); + CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Triggering info look up by project ID" ) ); + + const bool withAuth = r->request().attribute( static_cast( AttrAuthUsed ) ).toBool(); + const QNetworkReply *reply = getProjectsDetails( {projectId}, withAuth ); + connect( reply, &QNetworkReply::finished, this, &MerginApi::getProjectsDetailsReplyFinished ); + } - transaction.replyPullProjectInfo->deleteLater(); - transaction.replyPullProjectInfo = nullptr; - finishProjectSync( projectFullName, false ); } } -void MerginApi::prepareProjectPull( const QString &projectFullName, const QByteArray &data ) +void MerginApi::prepareProjectPull( const QString &projectId, const QByteArray &data ) { - Q_ASSERT( mTransactionalStatus.contains( projectFullName ) ); - TransactionStatus &transaction = mTransactionalStatus[projectFullName]; + Q_ASSERT( mTransactionalStatus.contains( projectId ) ); + TransactionStatus &transaction = mTransactionalStatus[projectId]; - MerginProjectMetadata serverProject = MerginProjectMetadata::fromJson( data ); + const MerginProjectMetadata serverProject = MerginProjectMetadata::fromJson( data ); transaction.projectMetadata = data; transaction.version = serverProject.version; - LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); + const LocalProject projectInfo = mLocalProjects.projectFromProjectId( projectId ); if ( projectInfo.isValid() ) { transaction.projectDir = projectInfo.projectDir; @@ -2472,28 +2603,25 @@ void MerginApi::prepareProjectPull( const QString &projectFullName, const QByteA // do not continue if we are already on the latest version if ( projectInfo.localVersion != -1 && projectInfo.localVersion == serverProject.version ) { - emit projectAlreadyOnLatestVersion( projectFullName ); - CoreUtils::log( QStringLiteral( "Pull %1" ).arg( projectFullName ), QStringLiteral( "Project is already on the latest version: %1" ).arg( serverProject.version ) ); + emit projectAlreadyOnLatestVersion( projectInfo.id() ); + CoreUtils::log( QStringLiteral( "Pull %1" ).arg( projectInfo.fullName() ), QStringLiteral( "Project is already on the latest version: %1" ).arg( serverProject.version ) ); - return finishProjectSync( projectFullName, false ); + return finishProjectSync( projectInfo.fullName(), projectId, false ); } } else { - QString projectNamespace; - QString projectName; - extractProjectName( projectFullName, projectNamespace, projectName ); - // remove any leftover temp files that could be created from previous unsuccessful download - removeProjectsTempFolder( projectNamespace, projectName ); + removeProjectsTempFolder( serverProject.projectNamespace, serverProject.name ); // project has not been downloaded yet - we need to create a directory for it - transaction.projectDir = CoreUtils::createUniqueProjectDirectory( mDataDir, projectName ); + transaction.projectDir = CoreUtils::createUniqueProjectDirectory( mDataDir, serverProject.name ); transaction.firstTimeDownload = true; // create file indicating first time download in progress - QString downloadInProgressFilePath = CoreUtils::downloadInProgressFilePath( transaction.projectDir ); + const QString downloadInProgressFilePath = CoreUtils::downloadInProgressFilePath( transaction.projectDir ); createPathIfNotExists( downloadInProgressFilePath ); + const QString projectFullName = CoreUtils::getFullProjectName( serverProject.projectNamespace, serverProject.name ); if ( !CoreUtils::createEmptyFile( downloadInProgressFilePath ) ) CoreUtils::log( QStringLiteral( "pull %1" ).arg( projectFullName ), "Unable to create temporary download in progress file" ); @@ -2504,23 +2632,24 @@ void MerginApi::prepareProjectPull( const QString &projectFullName, const QByteA if ( transaction.configAllowed ) { - prepareDownloadConfig( projectFullName ); + prepareDownloadConfig( projectId ); } else { - startProjectPull( projectFullName ); + startProjectPull( projectId ); } } -void MerginApi::startProjectPull( const QString &projectFullName ) +void MerginApi::startProjectPull( const QString &projectId ) { - Q_ASSERT( mTransactionalStatus.contains( projectFullName ) ); - TransactionStatus &transaction = mTransactionalStatus[projectFullName]; + Q_ASSERT( mTransactionalStatus.contains( projectId ) ); + TransactionStatus &transaction = mTransactionalStatus[projectId]; QList localFiles = getLocalProjectFiles( transaction.projectDir + "/" ); MerginProjectMetadata serverProject = MerginProjectMetadata::fromJson( transaction.projectMetadata ); MerginProjectMetadata oldServerProject = MerginProjectMetadata::fromCachedJson( transaction.projectDir + "/" + sMetadataFile ); MerginConfig oldTransactionConfig = MerginConfig::fromFile( transaction.projectDir + "/" + sMerginConfigFile ); + const QString projectFullName = CoreUtils::getFullProjectName( serverProject.projectNamespace, serverProject.name ); CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Updating from version %1 to version %2" ) .arg( oldServerProject.version ).arg( serverProject.version ) ); @@ -2536,7 +2665,7 @@ void MerginApi::startProjectPull( const QString &projectFullName ) CoreUtils::log( "pull " + projectFullName, transaction.diff.dump() ); - for ( QString filePath : transaction.diff.remoteAdded ) + for ( const QString &filePath : transaction.diff.remoteAdded ) { MerginFile file = serverProject.fileInfo( filePath ); QList items = itemsForFileChunks( file, transaction.version ); @@ -2544,11 +2673,11 @@ void MerginApi::startProjectPull( const QString &projectFullName ) transaction.gpkgSchemaChanged = true; } - for ( QString filePath : transaction.diff.remoteUpdated ) + for ( const QString &filePath : transaction.diff.remoteUpdated ) { MerginFile file = serverProject.fileInfo( filePath ); - // for diffable files - download and apply to the basefile (without rebase) + // for diffable files - download and apply to the base file (without rebase) if ( isFileDiffable( filePath ) && file.pullCanUseDiff ) { QList items = itemsForFileDiffs( file ); @@ -2563,11 +2692,11 @@ void MerginApi::startProjectPull( const QString &projectFullName ) } // also download files which were changed both on the server and locally (the local version will be renamed as conflicting copy) - for ( QString filePath : transaction.diff.conflictRemoteUpdatedLocalUpdated ) + for ( const QString &filePath : transaction.diff.conflictRemoteUpdatedLocalUpdated ) { MerginFile file = serverProject.fileInfo( filePath ); - // for diffable files - download and apply to the basefile (will also do rebase) + // for diffable files - download and apply to the base file (will also do rebase) if ( isFileDiffable( filePath ) && file.pullCanUseDiff ) { QList items = itemsForFileDiffs( file ); @@ -2582,7 +2711,7 @@ void MerginApi::startProjectPull( const QString &projectFullName ) } // also download files which were added both on the server and locally (the local version will be renamed as conflicting copy) - for ( QString filePath : transaction.diff.conflictRemoteAddedLocalAdded ) + for ( const QString &filePath : transaction.diff.conflictRemoteAddedLocalAdded ) { MerginFile file = serverProject.fileInfo( filePath ); QList items = itemsForFileChunks( file, transaction.version ); @@ -2591,7 +2720,7 @@ void MerginApi::startProjectPull( const QString &projectFullName ) } // schedule removed files to be deleted - for ( QString filePath : transaction.diff.remoteDeleted ) + for ( const QString &filePath : transaction.diff.remoteDeleted ) { transaction.pullTasks << PullTask( PullTask::Delete, filePath, QList() ); } @@ -2607,7 +2736,7 @@ void MerginApi::startProjectPull( const QString &projectFullName ) { totalSize += item.size; } - transaction.totalSize = totalSize; + transaction.totalSize = static_cast( totalSize ); // order download queue from the largest to smallest chunks to better // work with parallel downloads @@ -2629,21 +2758,21 @@ void MerginApi::startProjectPull( const QString &projectFullName ) if ( transaction.downloadQueue.isEmpty() ) { - finalizeProjectPull( projectFullName ); + finalizeProjectPull( projectId ); } else { while ( transaction.replyPullItems.count() < 5 && !transaction.downloadQueue.isEmpty() ) { - downloadNextItem( projectFullName ); + downloadNextItem( projectId ); } } } -void MerginApi::prepareDownloadConfig( const QString &projectFullName, bool downloaded ) +void MerginApi::prepareDownloadConfig( const QString &projectId, const bool downloaded ) { - Q_ASSERT( mTransactionalStatus.contains( projectFullName ) ); - TransactionStatus &transaction = mTransactionalStatus[projectFullName]; + Q_ASSERT( mTransactionalStatus.contains( projectId ) ); + TransactionStatus &transaction = mTransactionalStatus[projectId]; MerginProjectMetadata newServerVersion = MerginProjectMetadata::fromJson( transaction.projectMetadata ); @@ -2651,14 +2780,14 @@ void MerginApi::prepareDownloadConfig( const QString &projectFullName, bool down { return file.path == sMerginConfigFile; } ); - bool serverContainsConfig = res != newServerVersion.files.end(); + const bool serverContainsConfig = res != newServerVersion.files.end(); if ( serverContainsConfig ) { if ( !downloaded ) { - // we should have server config but we do not have it yet - return requestServerConfig( projectFullName ); + // we should have server config, but we do not have it yet + return requestServerConfig( projectId ); } } @@ -2669,7 +2798,7 @@ void MerginApi::prepareDownloadConfig( const QString &projectFullName, bool down return file.path == sMerginConfigFile; } ); - bool previousVersionContainedConfig = ( resOld != oldServerVersion.files.end() ) && !transaction.firstTimeDownload; + const bool previousVersionContainedConfig = resOld != oldServerVersion.files.end() && !transaction.firstTimeDownload; if ( !transaction.config.isValid ) { @@ -2680,8 +2809,8 @@ void MerginApi::prepareDownloadConfig( const QString &projectFullName, bool down else if ( serverContainsConfig && previousVersionContainedConfig ) { // config was there, check if there are changes - QString newChk = newServerVersion.fileInfo( sMerginConfigFile ).checksum; - QString oldChk = oldServerVersion.fileInfo( sMerginConfigFile ).checksum; + const QString newChk = newServerVersion.fileInfo( sMerginConfigFile ).checksum; + const QString oldChk = oldServerVersion.fileInfo( sMerginConfigFile ).checksum; if ( newChk == oldChk ) { @@ -2690,7 +2819,7 @@ void MerginApi::prepareDownloadConfig( const QString &projectFullName, bool down else { // config was changed, but what changed? - MerginConfig oldConfig = MerginConfig::fromFile( transaction.projectDir + "/" + MerginApi::sMerginConfigFile ); + const MerginConfig oldConfig = MerginConfig::fromFile( transaction.projectDir + "/" + sMerginConfigFile ); if ( oldConfig.selectiveSyncEnabled != transaction.config.selectiveSyncEnabled ) { @@ -2734,13 +2863,15 @@ void MerginApi::prepareDownloadConfig( const QString &projectFullName, bool down // if it would be possible to add mergin-config locally, it needs to be checked here } - startProjectPull( projectFullName ); + startProjectPull( projectId ); } -void MerginApi::requestServerConfig( const QString &projectFullName ) +void MerginApi::requestServerConfig( const QString &projectId ) { - Q_ASSERT( mTransactionalStatus.contains( projectFullName ) ); - TransactionStatus &transaction = mTransactionalStatus[projectFullName]; + Q_ASSERT( mTransactionalStatus.contains( projectId ) ); + TransactionStatus &transaction = mTransactionalStatus[projectId]; + const MerginProjectMetadata project = MerginProjectMetadata::fromJson( transaction.projectMetadata ); + const QString projectFullName = CoreUtils::getFullProjectName( project.projectNamespace, project.name ); QUrl url( mApiRoot + QStringLiteral( "/v1/project/raw/" ) + projectFullName ); QUrlQuery query; @@ -2751,7 +2882,7 @@ void MerginApi::requestServerConfig( const QString &projectFullName ) QNetworkRequest request = getDefaultRequest(); request.setUrl( url ); - request.setAttribute( static_cast( AttrProjectFullName ), projectFullName ); + request.setAttribute( static_cast( AttrProjectId ), projectId ); Q_ASSERT( !transaction.replyPullServerConfig ); transaction.replyPullServerConfig = mManager->get( request ); @@ -2760,13 +2891,13 @@ void MerginApi::requestServerConfig( const QString &projectFullName ) CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Requesting mergin config: " ) + url.toString() ); } -QList MerginApi::itemsForFileChunks( const MerginFile &file, int version ) +QList MerginApi::itemsForFileChunks( const MerginFile &file, const int version ) { QList lst; qint64 from = 0; while ( from < file.size ) { - qint64 size = qMin( MerginApi::UPLOAD_CHUNK_SIZE, file.size - from ); + const qint64 size = qMin( UPLOAD_CHUNK_SIZE, file.size - from ); lst << DownloadQueueItem( file.path, size, version, from, from + size - 1 ); from += size; } @@ -2792,8 +2923,8 @@ static MerginFile findFile( const QString &filePath, const QList &fi if ( merginFile.path == filePath ) return merginFile; } - CoreUtils::log( QStringLiteral( "MerginFile" ), QStringLiteral( "requested findFile() for non-existant file: %1" ).arg( filePath ) ); - return MerginFile(); + CoreUtils::log( QStringLiteral( "MerginFile" ), QStringLiteral( "requested findFile() for non-existent file: %1" ).arg( filePath ) ); + return {}; } @@ -2803,9 +2934,10 @@ void MerginApi::pushInfoReplyFinished() Q_ASSERT( r ); QString projectFullName = r->request().attribute( static_cast( AttrProjectFullName ) ).toString(); + QString projectId = r->request().attribute( static_cast( AttrProjectId ) ).toString(); - Q_ASSERT( mTransactionalStatus.contains( projectFullName ) ); - TransactionStatus &transaction = mTransactionalStatus[projectFullName]; + Q_ASSERT( mTransactionalStatus.contains( projectId ) ); + TransactionStatus &transaction = mTransactionalStatus[projectId]; Q_ASSERT( r == transaction.replyPushProjectInfo ); if ( r->error() == QNetworkReply::NoError ) @@ -2817,7 +2949,7 @@ void MerginApi::pushInfoReplyFinished() transaction.replyPushProjectInfo->deleteLater(); transaction.replyPushProjectInfo = nullptr; - LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); + LocalProject projectInfo = mLocalProjects.projectFromProjectId( projectId ); transaction.projectDir = projectInfo.projectDir; Q_ASSERT( !transaction.projectDir.isEmpty() ); @@ -2831,7 +2963,7 @@ void MerginApi::pushInfoReplyFinished() CoreUtils::log( "push " + projectFullName, QStringLiteral( "Need pull first: local version %1 | server version %2" ) .arg( projectInfo.localVersion ).arg( serverProject.version ) ); transaction.pullBeforePush = true; - prepareProjectPull( projectFullName, data ); + prepareProjectPull( projectId, data ); return; } @@ -2841,7 +2973,7 @@ void MerginApi::pushInfoReplyFinished() // Cache mergin-config, since we are on the most recent version, it is sufficient to just read the local version if ( transaction.configAllowed ) { - transaction.config = MerginConfig::fromFile( transaction.projectDir + "/" + MerginApi::sMerginConfigFile ); + transaction.config = MerginConfig::fromFile( transaction.projectDir + "/" + sMerginConfigFile ); } transaction.diff = compareProjectFiles( @@ -2860,19 +2992,19 @@ void MerginApi::pushInfoReplyFinished() QList filesToUpload; QList addedMerginFiles, updatedMerginFiles, deletedMerginFiles; QList diffFiles; - for ( QString filePath : transaction.diff.localAdded ) + for ( const QString &filePath : transaction.diff.localAdded ) { MerginFile merginFile = findFile( filePath, localFiles ); merginFile.chunks = generateChunkIdsForSize( merginFile.size ); addedMerginFiles.append( merginFile ); } - for ( QString filePath : transaction.diff.localUpdated ) + for ( const QString &filePath : transaction.diff.localUpdated ) { MerginFile merginFile = findFile( filePath, localFiles ); merginFile.chunks = generateChunkIdsForSize( merginFile.size ); - if ( MerginApi::isFileDiffable( filePath ) ) + if ( isFileDiffable( filePath ) ) { // try to create a diff QString diffName; @@ -2884,8 +3016,8 @@ void MerginApi::pushInfoReplyFinished() { QByteArray checksumDiff = CoreUtils::calculateChecksum( diffPath ); - // TODO: this is ugly. our basefile may not need to have the same checksum as the server's - // basefile (because each of them have applied the diff independently) so we have to fake it + // TODO: this is ugly. our base file may not need to have the same checksum as the server's + // base file (because each of them have applied the diff independently) so we have to fake it QByteArray checksumBase = serverProject.fileInfo( filePath ).checksum.toLatin1(); merginFile.diffName = diffName; @@ -2908,7 +3040,7 @@ void MerginApi::pushInfoReplyFinished() updatedMerginFiles.append( merginFile ); } - for ( QString filePath : transaction.diff.localDeleted ) + for ( const QString &filePath : transaction.diff.localDeleted ) { MerginFile merginFile = findFile( filePath, serverProject.files ); deletedMerginFiles.append( merginFile ); @@ -2920,7 +3052,7 @@ void MerginApi::pushInfoReplyFinished() transaction.projectMetadata = data; transaction.version = MerginProjectMetadata::fromJson( data ).version; - finishProjectSync( projectFullName, true ); + finishProjectSync( projectFullName, projectId, true ); return; } @@ -2940,7 +3072,7 @@ void MerginApi::pushInfoReplyFinished() changes.insert( "renamed", QJsonArray() ); qint64 totalSize = 0; - for ( MerginFile file : filesToUpload ) + for ( const MerginFile &file : filesToUpload ) { if ( !file.diffName.isEmpty() ) totalSize += file.diffSize; @@ -2951,7 +3083,7 @@ void MerginApi::pushInfoReplyFinished() CoreUtils::log( "push " + projectFullName, QStringLiteral( "%1 items to upload (total size %2 bytes)" ) .arg( filesToUpload.count() ).arg( totalSize ) ); - transaction.totalSize = totalSize; + transaction.totalSize = static_cast( totalSize ); transaction.pushQueue = filesToUpload; transaction.pushDiffFiles = diffFiles; @@ -2961,7 +3093,7 @@ void MerginApi::pushInfoReplyFinished() QJsonDocument jsonDoc; jsonDoc.setObject( json ); - pushStart( projectFullName, jsonDoc.toJson( QJsonDocument::Compact ) ); + pushStart( projectFullName, projectId, jsonDoc.toJson( QJsonDocument::Compact ) ); } else { @@ -2973,12 +3105,12 @@ void MerginApi::pushInfoReplyFinished() CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) ); int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); - emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: pushInfo" ), httpCode, projectFullName ); + emit networkErrorOccurred( serverMsg, httpCode, projectId ); transaction.replyPushProjectInfo->deleteLater(); transaction.replyPushProjectInfo = nullptr; - finishProjectSync( projectFullName, false ); + finishProjectSync( projectFullName, projectId, false ); } } @@ -2987,16 +3119,17 @@ void MerginApi::pushFinishReplyFinished() QNetworkReply *r = qobject_cast( sender() ); Q_ASSERT( r ); - QString projectFullName = r->request().attribute( static_cast( AttrProjectFullName ) ).toString(); + const QString projectFullName = r->request().attribute( static_cast( AttrProjectFullName ) ).toString(); + const QString projectId = r->request().attribute( static_cast( AttrProjectId ) ).toString(); - Q_ASSERT( mTransactionalStatus.contains( projectFullName ) ); - TransactionStatus &transaction = mTransactionalStatus[projectFullName]; + Q_ASSERT( mTransactionalStatus.contains( projectId ) ); + TransactionStatus &transaction = mTransactionalStatus[projectId]; Q_ASSERT( r == transaction.replyPushFinish ); if ( r->error() == QNetworkReply::NoError ) { - Q_ASSERT( mTransactionalStatus.contains( projectFullName ) ); - QByteArray data = r->readAll(); + Q_ASSERT( mTransactionalStatus.contains( projectId ) ); + const QByteArray data = r->readAll(); CoreUtils::log( "push " + projectFullName, QStringLiteral( "Transaction finish accepted" ) ); transaction.replyPushFinish->deleteLater(); @@ -3005,18 +3138,18 @@ void MerginApi::pushFinishReplyFinished() transaction.projectMetadata = data; transaction.version = MerginProjectMetadata::fromJson( data ).version; - // a new diffable files suppose to have their basefile copies in .mergin - for ( QString filePath : transaction.diff.localAdded ) + // a new diffable files suppose to have their base file copies in .mergin + for ( const QString &filePath : transaction.diff.localAdded ) { - if ( MerginApi::isFileDiffable( filePath ) ) + if ( isFileDiffable( filePath ) ) { - QString basefile = transaction.projectDir + "/.mergin/" + filePath; - createPathIfNotExists( basefile ); + QString baseFile = transaction.projectDir + "/.mergin/" + filePath; + createPathIfNotExists( baseFile ); QString sourcePath = transaction.projectDir + "/" + filePath; - if ( !QFile::copy( sourcePath, basefile ) ) + if ( !QFile::copy( sourcePath, baseFile ) ) { - CoreUtils::log( "push " + projectFullName, "failed to copy new basefile for: " + filePath ); + CoreUtils::log( "push " + projectFullName, "failed to copy new base file for: " + filePath ); } } } @@ -3027,16 +3160,16 @@ void MerginApi::pushFinishReplyFinished() { QString diffPath = transaction.projectDir + "/.mergin/" + merginFile.diffName; - // update basefile (unmodified file that should be equivalent to the server) + // update base file (unmodified file that should be equivalent to the server) QString basePath = transaction.projectDir + "/.mergin/" + merginFile.path; - bool res = GeodiffUtils::applyChangeset( basePath, diffPath ); + const bool res = GeodiffUtils::applyChangeset( basePath, diffPath ); if ( res ) { CoreUtils::log( "push " + projectFullName, QString( "Applied %1 to base file of %2" ).arg( merginFile.diffName, merginFile.path ) ); } else { - CoreUtils::log( "push " + projectFullName, QString( "Failed to apply changeset %1 to basefile %2 - error %3" ).arg( diffPath ).arg( basePath ).arg( res ) ); + CoreUtils::log( "push " + projectFullName, QString( "Failed to apply changeset %1 to base file %2 - error %3" ).arg( diffPath ).arg( basePath ).arg( res ) ); } // remove temporary diff files @@ -3044,7 +3177,7 @@ void MerginApi::pushFinishReplyFinished() CoreUtils::log( "push " + projectFullName, "Failed to remove diff: " + diffPath ); } - finishProjectSync( projectFullName, true ); + finishProjectSync( projectFullName, projectId, true ); } else { @@ -3052,11 +3185,11 @@ void MerginApi::pushFinishReplyFinished() if ( r->error() == QNetworkReply::OperationCanceledError ) serverMsg = sSyncCanceledMessage; - QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "pushFinish" ), r->errorString(), serverMsg ); + const QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "pushFinish" ), r->errorString(), serverMsg ); CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) ); - int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); - emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: pushFinish" ), httpCode, projectFullName ); + const int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); + emit networkErrorOccurred( serverMsg, httpCode, projectId ); // remove temporary diff files const auto diffFiles = transaction.pushDiffFiles; @@ -3070,7 +3203,7 @@ void MerginApi::pushFinishReplyFinished() transaction.replyPushFinish->deleteLater(); transaction.replyPushFinish = nullptr; - finishProjectSync( projectFullName, false ); + finishProjectSync( projectFullName, projectId, false ); } } @@ -3079,7 +3212,8 @@ void MerginApi::pushCancelReplyFinished() QNetworkReply *r = qobject_cast( sender() ); Q_ASSERT( r ); - QString projectFullName = r->request().attribute( static_cast( AttrProjectFullName ) ).toString(); + const QString projectId = r->request().attribute( static_cast( AttrProjectId ) ).toString(); + const QString projectFullName = mLocalProjects.projectFromProjectId( projectId ).fullName(); if ( r->error() == QNetworkReply::NoError ) { @@ -3088,11 +3222,11 @@ void MerginApi::pushCancelReplyFinished() else { QString serverMsg = extractServerErrorMsg( r->readAll() ); - QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "uploadCancel" ), r->errorString(), serverMsg ); + const QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "uploadCancel" ), r->errorString(), serverMsg ); CoreUtils::log( "push " + projectFullName, QStringLiteral( "FAILED - %1" ).arg( message ) ); } - emit pushCanceled( projectFullName, r->error() == QNetworkReply::NoError ); + emit pushCanceled( projectId ); r->deleteLater(); } @@ -3131,7 +3265,7 @@ void MerginApi::getUserInfoFinished() else { QString serverMsg = extractServerErrorMsg( r->readAll() ); - QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "getUserInfo" ), r->errorString(), serverMsg ); + const QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "getUserInfo" ), r->errorString(), serverMsg ); CoreUtils::log( "user info", QStringLiteral( "FAILED - %1" ).arg( message ) ); // This is an ugly fix for #3261: if the user was logged in, but the token was already expired @@ -3143,13 +3277,13 @@ void MerginApi::getUserInfoFinished() // flow of network requests. static bool firstTimeExpiredTokenAnd401 = true; if ( firstTimeExpiredTokenAnd401 && r->attribute( QNetworkRequest::HttpStatusCodeAttribute ) == 401 && - !mUserAuth->authToken().isEmpty() && mUserAuth->tokenExpiration() < QDateTime().currentDateTimeUtc() ) + !mUserAuth->authToken().isEmpty() && mUserAuth->tokenExpiration() < QDateTime::currentDateTimeUtc() ) { firstTimeExpiredTokenAnd401 = false; } else { - emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: getUserInfo" ) ); + emit networkErrorOccurred( serverMsg ); } } @@ -3166,10 +3300,10 @@ void MerginApi::getWorkspaceInfoReplyFinished() if ( r->error() == QNetworkReply::NoError ) { CoreUtils::log( "workspace info", QStringLiteral( "Success" ) ); - QJsonDocument doc = QJsonDocument::fromJson( r->readAll() ); + const QJsonDocument doc = QJsonDocument::fromJson( r->readAll() ); if ( doc.isObject() ) { - QJsonObject docObj = doc.object(); + const QJsonObject docObj = doc.object(); mWorkspaceInfo->setFromJson( docObj ); emit getWorkspaceInfoFinished(); @@ -3178,10 +3312,10 @@ void MerginApi::getWorkspaceInfoReplyFinished() else { QString serverMsg = extractServerErrorMsg( r->readAll() ); - QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "getWorkspaceInfo" ), r->errorString(), serverMsg ); + const QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "getWorkspaceInfo" ), r->errorString(), serverMsg ); CoreUtils::log( "workspace info", QStringLiteral( "FAILED - %1" ).arg( message ) ); mWorkspaceInfo->clear(); - emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: getWorkspaceInfo" ) ); + emit networkErrorOccurred( serverMsg ); } r->deleteLater(); @@ -3191,7 +3325,7 @@ bool MerginApi::hasLocalChanges( const QList &oldServerFiles, const QList &localFiles, const QString &projectDir, - const MerginConfig config + const MerginConfig &config ) { QList resolvedOldServerFiles; @@ -3224,37 +3358,35 @@ bool MerginApi::hasLocalChanges( for ( const MerginFile &localFile : localFiles ) { QString filePath = localFile.path; - bool hasOldServer = oldServerFilesMap.contains( localFile.path ); + const bool hasOldServer = oldServerFilesMap.contains( localFile.path ); if ( !hasOldServer ) { // L-A return true; } - else - { - const QString chkOld = oldServerFilesMap.value( localFile.path ).checksum; - const QString chkLocal = localFile.checksum; - if ( chkOld != chkLocal ) + const QString chkOld = oldServerFilesMap.value( localFile.path ).checksum; + const QString chkLocal = localFile.checksum; + + if ( chkOld != chkLocal ) + { + if ( isFileDiffable( filePath ) ) { - if ( isFileDiffable( filePath ) ) - { - // we need to do a diff here to figure out whether the file is actually changed or not - // because the real content may be the same although the checksums do not match - // e.g. when GPKG is opened, its header is updated and therefore lastModified timestamp/checksum is updated as well. - if ( GeodiffUtils::hasPendingChanges( projectDir, filePath ) ) - { - // L-U - return true; - } - } - else + // we need to do a diff here to figure out whether the file is actually changed or not + // because the real content may be the same although the checksums do not match + // e.g. when GPKG is opened, its header is updated and therefore lastModified timestamp/checksum is updated as well. + if ( GeodiffUtils::hasPendingChanges( projectDir, filePath ) ) { // L-U return true; } } + else + { + // L-U + return true; + } } } @@ -3277,16 +3409,16 @@ ProjectDiff MerginApi::compareProjectFiles( ProjectDiff diff; QHash oldServerFilesMap, newServerFilesMap; - for ( MerginFile file : newServerFiles ) + for ( const MerginFile &file : newServerFiles ) { newServerFilesMap.insert( file.path, file ); } - for ( MerginFile file : oldServerFiles ) + for ( const MerginFile &file : oldServerFiles ) { oldServerFilesMap.insert( file.path, file ); } - for ( MerginFile localFile : localFiles ) + for ( const MerginFile &localFile : localFiles ) { QString filePath = localFile.path; bool hasOldServer = oldServerFilesMap.contains( localFile.path ); @@ -3388,7 +3520,7 @@ ProjectDiff MerginApi::compareProjectFiles( } // go through files listed on the server, but not available locally - for ( MerginFile file : newServerFilesMap ) + for ( const MerginFile &file : newServerFilesMap ) { bool hasOldServer = oldServerFilesMap.contains( file.path ); @@ -3427,7 +3559,7 @@ ProjectDiff MerginApi::compareProjectFiles( // R-A if ( allowConfig ) { - if ( MerginApi::excludeFromSync( file.path, config ) ) + if ( excludeFromSync( file.path, config ) ) { continue; } @@ -3468,6 +3600,7 @@ MerginProject MerginApi::parseProjectMetadata( const QJsonObject &proj ) project.projectName = proj.value( QStringLiteral( "name" ) ).toString(); project.projectNamespace = proj.value( QStringLiteral( "namespace" ) ).toString(); + project.projectId = proj.value( QStringLiteral( "id" ) ).toString(); QString versionStr = proj.value( QStringLiteral( "version" ) ).toString(); if ( versionStr.isEmpty() ) @@ -3480,7 +3613,7 @@ MerginProject MerginApi::parseProjectMetadata( const QJsonObject &proj ) project.serverVersion = versionStr.toInt(); } - QDateTime updated = QDateTime::fromString( proj.value( QStringLiteral( "updated" ) ).toString(), Qt::ISODateWithMs ).toUTC(); + const QDateTime updated = QDateTime::fromString( proj.value( QStringLiteral( "updated" ) ).toString(), Qt::ISODateWithMs ).toUTC(); if ( !updated.isValid() ) { project.serverUpdated = QDateTime::fromString( proj.value( QStringLiteral( "created" ) ).toString(), Qt::ISODateWithMs ).toUTC(); @@ -3496,21 +3629,21 @@ MerginProject MerginApi::parseProjectMetadata( const QJsonObject &proj ) MerginProjectsList MerginApi::parseProjectsFromJson( const QJsonDocument &doc ) { if ( !doc.isObject() ) - return MerginProjectsList(); + return {}; QJsonObject object = doc.object(); MerginProjectsList result; if ( object.contains( "projects" ) && object.value( "projects" ).isArray() ) // listProjects API { - QJsonArray vArray = object.value( "projects" ).toArray(); + const QJsonArray vArray = object.value( "projects" ).toArray(); for ( auto it = vArray.constBegin(); it != vArray.constEnd(); ++it ) { result << parseProjectMetadata( it->toObject() ); } } - else if ( !object.isEmpty() ) // listProjectsbyName API returns projects as separate objects not in array + else if ( !object.isEmpty() ) // listProjectsByName API returns projects as separate objects not in array { for ( auto it = object.begin(); it != object.end(); ++it ) { @@ -3518,7 +3651,7 @@ MerginProjectsList MerginApi::parseProjectsFromJson( const QJsonDocument &doc ) if ( !project.remoteError.isEmpty() ) { // add project namespace/name from object name in case of error - MerginApi::extractProjectName( it.key(), project.projectNamespace, project.projectName ); + CoreUtils::extractProjectName( it.key(), project.projectNamespace, project.projectName ); } result << project; } @@ -3550,9 +3683,9 @@ void MerginApi::refreshAuthToken() } } -QStringList MerginApi::generateChunkIdsForSize( qint64 fileSize ) +QStringList MerginApi::generateChunkIdsForSize( const qint64 fileSize ) { - qreal rawNoOfChunks = qreal( fileSize ) / UPLOAD_CHUNK_SIZE; + const qreal rawNoOfChunks = static_cast( fileSize ) / UPLOAD_CHUNK_SIZE; int noOfChunks = qCeil( rawNoOfChunks ); // edge case when file is empty, filesize equals zero @@ -3573,7 +3706,7 @@ QJsonArray MerginApi::prepareUploadChangesJSON( const QList &files ) { QJsonArray jsonArray; - for ( MerginFile file : files ) + for ( const MerginFile &file : files ) { QJsonObject fileObject; fileObject.insert( "path", file.path ); @@ -3598,7 +3731,7 @@ QJsonArray MerginApi::prepareUploadChangesJSON( const QList &files ) } QJsonArray chunksJson; - for ( QString id : file.chunks ) + for ( const QString &id : file.chunks ) { chunksJson.append( id ); } @@ -3608,20 +3741,20 @@ QJsonArray MerginApi::prepareUploadChangesJSON( const QList &files ) return jsonArray; } -void MerginApi::finishProjectSync( const QString &projectFullName, bool syncSuccessful ) +void MerginApi::finishProjectSync( const QString &projectFullName, const QString &projectId, const bool syncSuccessful ) { - Q_ASSERT( mTransactionalStatus.contains( projectFullName ) ); - TransactionStatus &transaction = mTransactionalStatus[projectFullName]; + Q_ASSERT( mTransactionalStatus.contains( projectId ) ); + const TransactionStatus &transaction = mTransactionalStatus[projectId]; - emit syncProjectStatusChanged( projectFullName, -1 ); // -1 means there's no sync going on + emit syncProjectStatusChanged( projectId, -1 ); // -1 means there's no sync going on if ( syncSuccessful ) { // update the local metadata file - writeData( transaction.projectMetadata, transaction.projectDir + "/" + MerginApi::sMetadataFile ); + writeData( transaction.projectMetadata, transaction.projectDir + "/" + sMetadataFile ); // update info of local projects - mLocalProjects.updateLocalVersion( transaction.projectDir, transaction.version ); + mLocalProjects.updateLocalVersion( projectId, transaction.version ); CoreUtils::log( "sync " + projectFullName, QStringLiteral( "### Finished ### New project version: %1\n" ).arg( transaction.version ) ); } @@ -3630,33 +3763,31 @@ void MerginApi::finishProjectSync( const QString &projectFullName, bool syncSucc CoreUtils::log( "sync " + projectFullName, QStringLiteral( "### FAILED ###\n" ) ); } - bool pullBeforePush = transaction.pullBeforePush; + const bool pullBeforePush = transaction.pullBeforePush; QString projectDir = transaction.projectDir; // keep it before the transaction gets removed - ProjectDiff diff = transaction.diff; - int newVersion = syncSuccessful ? transaction.version : -1; + const ProjectDiff diff = transaction.diff; + const int newVersion = syncSuccessful ? transaction.version : -1; if ( transaction.gpkgSchemaChanged || projectFileHasBeenUpdated( diff ) ) { - emit projectReloadNeededAfterSync( projectFullName ); + emit projectReloadNeededAfterSync( projectId ); } - mTransactionalStatus.remove( projectFullName ); + mTransactionalStatus.remove( projectId ); if ( pullBeforePush ) { CoreUtils::log( "sync " + projectFullName, QStringLiteral( "Continue with push after pull" ) ); // we're done only with the download part before the actual upload - so let's continue with upload - QString projectNamespace, projectName; - extractProjectName( projectFullName, projectNamespace, projectName ); - pushProject( projectNamespace, projectName ); + pushProject( projectFullName, projectId ); } else { - emit syncProjectFinished( projectFullName, syncSuccessful, newVersion ); + emit syncProjectFinished( projectId, syncSuccessful, newVersion ); if ( syncSuccessful ) { - emit projectDataChanged( projectFullName ); + emit projectDataChanged( projectFullName, projectId ); } } } @@ -3676,25 +3807,28 @@ bool MerginApi::writeData( const QByteArray &data, const QString &path ) return true; } -bool MerginApi::updateCachedProjectRole( const QString &projectFullName, const QString &newRole ) +bool MerginApi::updateCachedProjectRole( const QString &projectId, const QString &newRole ) const { - LocalProject project = mLocalProjects.projectFromMerginName( projectFullName ); + const LocalProject project = mLocalProjects.projectFromProjectId( projectId ); if ( !project.isValid() ) { return false; } - QString metadataPath = project.projectDir + "/" + sMetadataFile; + const QString metadataPath = project.projectDir + "/" + sMetadataFile; return CoreUtils::replaceValueInJson( metadataPath, "role", newRole ); } void MerginApi::createPathIfNotExists( const QString &filePath ) { - QDir dir; + const QDir dir; if ( !dir.exists( mDataDir ) ) - dir.mkpath( mDataDir ); + if ( !dir.mkpath( mDataDir ) ) + { + qDebug() << QString( "Failed to create directory for %1" ).arg( mDataDir ); + }; - QFileInfo newFile( filePath ); + const QFileInfo newFile( filePath ); if ( !newFile.absoluteDir().exists() ) { if ( !dir.mkpath( newFile.absolutePath() ) ) @@ -3713,9 +3847,9 @@ bool MerginApi::excludeFromSync( const QString &filePath, const MerginConfig &co { if ( config.isValid && config.selectiveSyncEnabled ) { - QFileInfo info( filePath ); + const QFileInfo info( filePath ); - bool isExcludedFormat = sIgnoreImageExtensions.contains( info.suffix().toLower() ); + const bool isExcludedFormat = sIgnoreImageExtensions.contains( info.suffix().toLower() ); if ( !isExcludedFormat ) return false; @@ -3724,7 +3858,7 @@ bool MerginApi::excludeFromSync( const QString &filePath, const MerginConfig &co { return true; // we are ignoring photos in the entire project } - else if ( filePath.startsWith( config.selectiveSyncDir ) ) + if ( filePath.startsWith( config.selectiveSyncDir ) ) { return true; // we are ignoring photo in subfolder } @@ -3732,16 +3866,16 @@ bool MerginApi::excludeFromSync( const QString &filePath, const MerginConfig &co return false; } -QSet MerginApi::listFiles( const QString &path ) +QSet MerginApi::listFiles( const QString &projectPath ) { QSet files; - QDirIterator it( path, QStringList() << QStringLiteral( "*" ), QDir::Files, QDirIterator::Subdirectories ); + QDirIterator it( projectPath, QStringList() << QStringLiteral( "*" ), QDir::Files, QDirIterator::Subdirectories ); while ( it.hasNext() ) { it.next(); if ( !isInIgnore( it.fileInfo() ) ) { - files << it.filePath().replace( path, "" ); + files << it.filePath().replace( projectPath, "" ); } } return files; @@ -3755,10 +3889,10 @@ void MerginApi::deleteAccount() } QNetworkRequest request = getDefaultRequest(); - QUrl url( mApiRoot + QStringLiteral( "/v1/user" ) ); + const QUrl url( mApiRoot + QStringLiteral( "/v1/user" ) ); request.setUrl( url ); - QNetworkReply *reply = mManager->deleteResource( request ); - connect( reply, &QNetworkReply::finished, this, [this]() { this->deleteAccountFinished();} ); + const QNetworkReply *reply = mManager->deleteResource( request ); + connect( reply, &QNetworkReply::finished, this, [this] { this->deleteAccountFinished();} ); CoreUtils::log( "delete account " + mUserInfo->username(), QStringLiteral( "Requesting account deletion: " ) + url.toString() ); } @@ -3772,11 +3906,7 @@ void MerginApi::deleteAccountFinished() CoreUtils::log( "delete account " + mUserInfo->username(), QStringLiteral( "Success" ) ); // remove all local projects from the device - LocalProjectsList projects = mLocalProjects.projects(); - for ( const LocalProject &info : projects ) - { - mLocalProjects.removeLocalProject( info.id() ); - } + mLocalProjects.projects().clear(); clearAuth(); @@ -3784,8 +3914,8 @@ void MerginApi::deleteAccountFinished() } else { - int statusCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); - QString serverMsg = extractServerErrorMsg( r->readAll() ); + const int statusCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); + const QString serverMsg = extractServerErrorMsg( r->readAll() ); CoreUtils::log( "delete account " + mUserInfo->username(), QStringLiteral( "FAILED - %1 %2. %3" ).arg( statusCode ).arg( r->errorString() ).arg( serverMsg ) ); if ( statusCode == 422 ) { @@ -3793,7 +3923,7 @@ void MerginApi::deleteAccountFinished() } else { - emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: deleteAccount" ) ); + emit networkErrorOccurred( serverMsg ); } emit accountDeleted( false ); @@ -3805,11 +3935,11 @@ void MerginApi::deleteAccountFinished() void MerginApi::getServerConfig() { QNetworkRequest request = getDefaultRequest(); - QString urlString = mApiRoot + QStringLiteral( "/config" ); - QUrl url( urlString ); + const QString urlString = mApiRoot + QStringLiteral( "/config" ); + const QUrl url( urlString ); request.setUrl( url ); - QNetworkReply *reply = mManager->get( request ); + const QNetworkReply *reply = mManager->get( request ); connect( reply, &QNetworkReply::finished, this, &MerginApi::getServerConfigReplyFinished ); CoreUtils::log( "Config", QStringLiteral( "Requesting server configuration: " ) + url.toString() ); @@ -3823,14 +3953,14 @@ void MerginApi::getServerConfigReplyFinished() if ( r->error() == QNetworkReply::NoError ) { CoreUtils::log( "Config", QStringLiteral( "Success" ) ); - QJsonDocument doc = QJsonDocument::fromJson( r->readAll() ); + const QJsonDocument doc = QJsonDocument::fromJson( r->readAll() ); if ( doc.isObject() ) { - QString serverType = doc.object().value( QStringLiteral( "server_type" ) ).toString(); - QString apiVersion = doc.object().value( QStringLiteral( "version" ) ).toString(); + const QString serverType = doc.object().value( QStringLiteral( "server_type" ) ).toString(); + const QString apiVersion = doc.object().value( QStringLiteral( "version" ) ).toString(); int major = -1; int minor = -1; - bool validVersion = parseVersion( apiVersion, major, minor ); + const bool validVersion = parseVersion( apiVersion, major, minor ); if ( !validVersion ) { @@ -3863,7 +3993,7 @@ void MerginApi::getServerConfigReplyFinished() } // will be dropped support for old servers (mostly CE servers without workspaces) - if ( ( MINIMUM_SERVER_VERSION_MAJOR == major && MINIMUM_SERVER_VERSION_MINOR > minor ) || ( MINIMUM_SERVER_VERSION_MAJOR > major ) ) + if ( ( MINIMUM_SERVER_VERSION_MAJOR == major && MINIMUM_SERVER_VERSION_MINOR > minor ) || MINIMUM_SERVER_VERSION_MAJOR > major ) { emit migrationRequested( QString( "%1.%2" ).arg( major ).arg( minor ) ); } @@ -3874,7 +4004,7 @@ void MerginApi::getServerConfigReplyFinished() } else { - int statusCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); + const int statusCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); if ( statusCode == 404 ) // legacy (old) server { setServerType( MerginServerType::OLD ); @@ -3883,8 +4013,8 @@ void MerginApi::getServerConfigReplyFinished() else { QString serverMsg = extractServerErrorMsg( r->readAll() ); - QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "getServerType" ), r->errorString(), serverMsg ); - emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: getServerType" ) ); + const QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "getServerType" ), r->errorString(), serverMsg ); + emit networkErrorOccurred( serverMsg ); CoreUtils::log( "server type", QStringLiteral( "FAILED - %1" ).arg( message ) ); } } @@ -3924,11 +4054,11 @@ void MerginApi::listWorkspaces() return; } - QUrl url( mApiRoot + QStringLiteral( "/v1/workspaces" ) ); + const QUrl url( mApiRoot + QStringLiteral( "/v1/workspaces" ) ); QNetworkRequest request = getDefaultRequest( mUserAuth->hasAuthData() ); request.setUrl( url ); - QNetworkReply *reply = mManager->get( request ); + const QNetworkReply *reply = mManager->get( request ); CoreUtils::log( "list workspaces", QStringLiteral( "Requesting: " ) + url.toString() ); connect( reply, &QNetworkReply::finished, this, &MerginApi::listWorkspacesReplyFinished ); } @@ -3941,11 +4071,11 @@ void MerginApi::listWorkspacesReplyFinished() if ( r->error() == QNetworkReply::NoError ) { CoreUtils::log( "list workspaces", QStringLiteral( "Success" ) ); - QJsonDocument doc = QJsonDocument::fromJson( r->readAll() ); + const QJsonDocument doc = QJsonDocument::fromJson( r->readAll() ); if ( doc.isArray() ) { QMap workspaces; - QJsonArray array = doc.array(); + const QJsonArray array = doc.array(); for ( auto it = array.constBegin(); it != array.constEnd(); ++it ) { QJsonObject ws = it->toObject(); @@ -3964,9 +4094,9 @@ void MerginApi::listWorkspacesReplyFinished() else { QString serverMsg = extractServerErrorMsg( r->readAll() ); - QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listWorkspaces" ), r->errorString(), serverMsg ); + const QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listWorkspaces" ), r->errorString(), serverMsg ); CoreUtils::log( "list workspaces", QStringLiteral( "FAILED - %1" ).arg( message ) ); - emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: listWorkspaces" ) ); + emit networkErrorOccurred( serverMsg ); emit listWorkspacesFailed(); } @@ -3981,11 +4111,11 @@ void MerginApi::listInvitations() return; } - QUrl url( mApiRoot + QStringLiteral( "/v1/workspace/invitations" ) ); + const QUrl url( mApiRoot + QStringLiteral( "/v1/workspace/invitations" ) ); QNetworkRequest request = getDefaultRequest( mUserAuth->hasAuthData() ); request.setUrl( url ); - QNetworkReply *reply = mManager->get( request ); + const QNetworkReply *reply = mManager->get( request ); CoreUtils::log( "list invitations", QStringLiteral( "Requesting: " ) + url.toString() ); connect( reply, &QNetworkReply::finished, this, &MerginApi::listInvitationsReplyFinished ); } @@ -3998,11 +4128,11 @@ void MerginApi::listInvitationsReplyFinished() if ( r->error() == QNetworkReply::NoError ) { CoreUtils::log( "list invitations", QStringLiteral( "Success" ) ); - QJsonDocument doc = QJsonDocument::fromJson( r->readAll() ); + const QJsonDocument doc = QJsonDocument::fromJson( r->readAll() ); if ( doc.isArray() ) { QList invitations; - QJsonArray array = doc.array(); + const QJsonArray array = doc.array(); for ( auto it = array.constBegin(); it != array.constEnd(); ++it ) { MerginInvitation invite = MerginInvitation::fromJsonObject( it->toObject() ); @@ -4019,16 +4149,16 @@ void MerginApi::listInvitationsReplyFinished() else { QString serverMsg = extractServerErrorMsg( r->readAll() ); - QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listInvitations" ), r->errorString(), serverMsg ); + const QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "listInvitations" ), r->errorString(), serverMsg ); CoreUtils::log( "list invitations", QStringLiteral( "FAILED - %1" ).arg( message ) ); - emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: listInvitations" ) ); + emit networkErrorOccurred( serverMsg ); emit listInvitationsFailed(); } r->deleteLater(); } -void MerginApi::processInvitation( const QString &uuid, bool accept ) +void MerginApi::processInvitation( const QString &uuid, const bool accept ) { if ( !validateAuth() || mApiVersionStatus != MerginApiStatus::OK ) { @@ -4037,8 +4167,8 @@ void MerginApi::processInvitation( const QString &uuid, bool accept ) } QNetworkRequest request = getDefaultRequest( true ); - QString urlString = mApiRoot + QStringLiteral( "/v1/workspace/invitation/%1" ).arg( uuid ); - QUrl url( urlString ); + const QString urlString = mApiRoot + QStringLiteral( "/v1/workspace/invitation/%1" ).arg( uuid ); + const QUrl url( urlString ); request.setUrl( url ); request.setRawHeader( "Content-Type", "application/json" ); request.setAttribute( static_cast( AttrAcceptFlag ), accept ); @@ -4047,8 +4177,8 @@ void MerginApi::processInvitation( const QString &uuid, bool accept ) QJsonObject jsonObject; jsonObject.insert( QStringLiteral( "accept" ), accept ); jsonDoc.setObject( jsonObject ); - QByteArray json = jsonDoc.toJson( QJsonDocument::Compact ); - QNetworkReply *reply = mManager->post( request, json ); + const QByteArray json = jsonDoc.toJson( QJsonDocument::Compact ); + const QNetworkReply *reply = mManager->post( request, json ); CoreUtils::log( "process invitation", QStringLiteral( "Requesting: " ) + url.toString() ); connect( reply, &QNetworkReply::finished, this, &MerginApi::processInvitationReplyFinished ); } @@ -4058,7 +4188,7 @@ void MerginApi::processInvitationReplyFinished() QNetworkReply *r = qobject_cast( sender() ); Q_ASSERT( r ); - bool accept = r->request().attribute( static_cast( AttrAcceptFlag ) ).toBool(); + const bool accept = r->request().attribute( static_cast( AttrAcceptFlag ) ).toBool(); if ( r->error() == QNetworkReply::NoError ) { @@ -4067,9 +4197,9 @@ void MerginApi::processInvitationReplyFinished() else { QString serverMsg = extractServerErrorMsg( r->readAll() ); - QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "processInvitation" ), r->errorString(), serverMsg ); + const QString message = QStringLiteral( "Network API error: %1(): %2. %3" ).arg( QStringLiteral( "processInvitation" ), r->errorString(), serverMsg ); CoreUtils::log( "process invitation", QStringLiteral( "FAILED - %1" ).arg( message ) ); - emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: processInvitation" ) ); + emit networkErrorOccurred( serverMsg ); emit processInvitationFailed(); } @@ -4082,7 +4212,8 @@ bool MerginApi::createWorkspace( const QString &workspaceName ) { if ( !validateAuth() ) { - emit missingAuthorizationError( workspaceName ); + // this should never happen, we shouldn't be able to see the page to create new workspace without authorization + emit notifyError( tr( "Please login again to create new workspace!" ) ); return false; } @@ -4098,7 +4229,7 @@ bool MerginApi::createWorkspace( const QString &workspaceName ) } QNetworkRequest request = getDefaultRequest(); - QUrl url( mApiRoot + QString( "/v1/workspace" ) ); + const QUrl url( mApiRoot + QString( "/v1/workspace" ) ); request.setUrl( url ); request.setRawHeader( "Content-Type", "application/json" ); request.setRawHeader( "Accept", "application/json" ); @@ -4108,9 +4239,9 @@ bool MerginApi::createWorkspace( const QString &workspaceName ) QJsonObject jsonObject; jsonObject.insert( QStringLiteral( "name" ), workspaceName ); jsonDoc.setObject( jsonObject ); - QByteArray json = jsonDoc.toJson( QJsonDocument::Compact ); + const QByteArray json = jsonDoc.toJson( QJsonDocument::Compact ); - QNetworkReply *reply = mManager->post( request, json ); + const QNetworkReply *reply = mManager->post( request, json ); connect( reply, &QNetworkReply::finished, this, &MerginApi::createWorkspaceReplyFinished ); CoreUtils::log( "create " + workspaceName, QStringLiteral( "Requesting workspace creation: " ) + url.toString() ); @@ -4143,7 +4274,7 @@ void MerginApi::createWorkspaceReplyFinished() QNetworkReply *r = qobject_cast( sender() ); Q_ASSERT( r ); - QString workspaceName = r->request().attribute( static_cast( AttrWorkspaceName ) ).toString(); + const QString workspaceName = r->request().attribute( static_cast( AttrWorkspaceName ) ).toString(); if ( r->error() == QNetworkReply::NoError ) { @@ -4153,51 +4284,48 @@ void MerginApi::createWorkspaceReplyFinished() else { QString serverMsg = extractServerErrorMsg( r->readAll() ); - QString message = QStringLiteral( "FAILED - %1: %2" ).arg( r->errorString(), serverMsg ); + const QString message = QStringLiteral( "FAILED - %1: %2" ).arg( r->errorString(), serverMsg ); CoreUtils::log( "create " + workspaceName, message ); - int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); + const int httpCode = r->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); if ( httpCode == 409 ) { - emit networkErrorOccurred( tr( "Workspace %1 already exists" ).arg( workspaceName ), QStringLiteral( "Mergin API error: createWorkspace" ), httpCode, workspaceName ); + emit networkErrorOccurred( tr( "Workspace %1 already exists" ).arg( workspaceName ), httpCode ); } else { - emit networkErrorOccurred( serverMsg, QStringLiteral( "Mergin API error: createWorkspace" ), httpCode, workspaceName ); + emit networkErrorOccurred( serverMsg, httpCode ); } } r->deleteLater(); } -bool MerginApi::apiSupportsWorkspaces() +bool MerginApi::apiSupportsWorkspaces() const { if ( mServerType == MerginServerType::SAAS || mServerType == MerginServerType::EE ) { return true; } - else - { - return false; - } + return false; } -DownloadQueueItem::DownloadQueueItem( const QString &fp, qint64 s, int v, qint64 rf, qint64 rt, bool diff ) - : filePath( fp ), size( s ), version( v ), rangeFrom( rf ), rangeTo( rt ), downloadDiff( diff ) +DownloadQueueItem::DownloadQueueItem( QString fp, const qint64 s, const int v, const qint64 rf, const qint64 rt, const bool diff ) + : filePath( std::move( fp ) ), size( s ), version( v ), rangeFrom( rf ), rangeTo( rt ), downloadDiff( diff ) { tempFileName = CoreUtils::uuidWithoutBraces( QUuid::createUuid() ); } -void MerginApi::reloadProjectRole( const QString &projectFullName ) +void MerginApi::reloadProjectRole( const QString &projectId ) { - if ( projectFullName.isEmpty() ) + if ( projectId.isEmpty() ) return; - QNetworkReply *reply = getProjectInfo( projectFullName, mUserAuth->hasAuthData() ); + //withAuth depends on whether user is logged in or not + const QNetworkReply *reply = getProjectDetails( projectId, mUserAuth->hasAuthData() ); if ( !reply ) return; - reply->request().setAttribute( static_cast( AttrProjectFullName ), projectFullName ); connect( reply, &QNetworkReply::finished, this, &MerginApi::reloadProjectRoleReplyFinished ); } @@ -4206,38 +4334,38 @@ void MerginApi::reloadProjectRoleReplyFinished() QNetworkReply *r = qobject_cast( sender() ); Q_ASSERT( r ); - QString projectFullName = r->request().attribute( static_cast( AttrProjectFullName ) ).toString(); - QString cachedRole = MerginApi::getCachedProjectRole( projectFullName ); + const QString projectId = r->request().attribute( static_cast( AttrProjectId ) ).toString(); + const QString cachedRole = getCachedProjectRole( projectId ); if ( r->error() == QNetworkReply::NoError ) { - QByteArray data = r->readAll(); - MerginProjectMetadata serverProject = MerginProjectMetadata::fromJson( data ); - QString role = serverProject.role; + const QByteArray data = r->readAll(); + const MerginProjectMetadata serverProject = MerginProjectMetadata::fromJson( data ); + const QString role = serverProject.role; if ( role != cachedRole ) { - if ( updateCachedProjectRole( projectFullName, role ) ) - emit projectRoleUpdated( projectFullName, role ); + if ( updateCachedProjectRole( projectId, role ) ) + emit projectRoleUpdated( projectId, role ); } } else { - CoreUtils::log( "Metadata", QString( "Failed to update cached role for project %1 - likely due to missing auth or you are offline" ).arg( projectFullName ) ); + CoreUtils::log( "Metadata", QString( "Failed to update cached role for project %1 - likely due to missing auth or you are offline" ).arg( projectId ) ); } r->deleteLater(); } -QString MerginApi::getCachedProjectRole( const QString &projectFullName ) const +QString MerginApi::getCachedProjectRole( const QString &projectId ) const { - if ( projectFullName.isEmpty() ) - return QString(); + if ( projectId.isEmpty() ) + return {}; - QString projectDir = mLocalProjects.projectFromMerginName( projectFullName ).projectDir; + const QString projectDir = mLocalProjects.projectFromProjectId( projectId ).projectDir; if ( projectDir.isEmpty() ) - return QString(); + return {}; MerginProjectMetadata cachedProjectMetadata = MerginProjectMetadata::fromCachedJson( projectDir + "/" + sMetadataFile ); @@ -4323,21 +4451,21 @@ void MerginApi::abortSsoFlow() mOauth2ReplyHandler->close(); } -bool MerginApi::isRetryableNetworkError( QNetworkReply *reply ) +bool MerginApi::isRetryableNetworkError( const QNetworkReply *reply ) { Q_ASSERT( reply ); - QNetworkReply::NetworkError err = reply->error(); - - bool isRetryableError = ( err == QNetworkReply::TimeoutError || - err == QNetworkReply::TemporaryNetworkFailureError || - err == QNetworkReply::NetworkSessionFailedError || - err == QNetworkReply::UnknownNetworkError || - err == QNetworkReply::RemoteHostClosedError || - err == QNetworkReply::ProxyConnectionClosedError || - err == QNetworkReply::ProxyTimeoutError || - err == QNetworkReply::UnknownProxyError || - err == QNetworkReply::ServiceUnavailableError ); + const QNetworkReply::NetworkError err = reply->error(); + + const bool isRetryableError = ( err == QNetworkReply::TimeoutError || + err == QNetworkReply::TemporaryNetworkFailureError || + err == QNetworkReply::NetworkSessionFailedError || + err == QNetworkReply::UnknownNetworkError || + err == QNetworkReply::RemoteHostClosedError || + err == QNetworkReply::ProxyConnectionClosedError || + err == QNetworkReply::ProxyTimeoutError || + err == QNetworkReply::UnknownProxyError || + err == QNetworkReply::ServiceUnavailableError ); return isRetryableError; } diff --git a/core/merginapi.h b/core/merginapi.h index 612757f77..5fd548ca2 100644 --- a/core/merginapi.h +++ b/core/merginapi.h @@ -12,22 +12,16 @@ #include -#include -#include #include #include -#include -#include -#include #include #include #include -#include +#include #include #include "merginapistatus.h" #include "merginservertype.h" -#include "merginsubscriptionstatus.h" #include "merginerrortypes.h" #include "merginprojectmetadata.h" #include "localprojectsmanager.h" @@ -107,7 +101,7 @@ struct ProjectDiff */ struct DownloadQueueItem { - DownloadQueueItem( const QString &fp, qint64 s, int v, qint64 rf = -1, qint64 rt = -1, bool diff = false ); + DownloadQueueItem( QString fp, qint64 s, int v, qint64 rf = -1, qint64 rt = -1, bool diff = false ); QString filePath; //!< path within the project qint64 size; //!< size of the item in bytes @@ -133,8 +127,8 @@ struct PullTask Delete, //!< remove files that have been removed from the server }; - PullTask( Method m, const QString &fp, const QList &d ) - : method( m ), filePath( fp ), data( d ) {} + PullTask( const Method m, QString fp, const QList &d ) + : method( m ), filePath( std::move( fp ) ), data( d ) {} Method method; //!< what to do with the file QString filePath; //!< what is the file path within project @@ -175,7 +169,7 @@ struct TransactionStatus // retry handling int retryCount = 0; //!< current number of retry attempts for failed network requests - static const int MAX_RETRY_COUNT = 5; //!< maximum number of retry attempts for failed network requests + static constexpr int MAX_RETRY_COUNT = 5; //!< maximum number of retry attempts for failed network requests QString projectDir; QByteArray projectMetadata; //!< metadata of the new project (not parsed) @@ -230,7 +224,7 @@ class MerginApi: public QObject public: explicit MerginApi( LocalProjectsManager &localProjects, QObject *parent = nullptr ); - ~MerginApi() = default; + ~MerginApi() override = default; MerginUserAuth *userAuth() const; MerginUserInfo *userInfo() const; @@ -239,7 +233,7 @@ class MerginApi: public QObject /** * Returns path of the local directory in which all projects are stored. - * Each project is one sub-directory. + * Each project is one subdirectory. * \note returns the directory without a trailing slash */ QString projectsPath() const { return mDataDir; } @@ -253,62 +247,70 @@ class MerginApi: public QObject * for "exploring" all public projects. However, it can be applied to fetch more results. * Eventually emits listProjectsFinished on which ProjectPanel (qml component) updates content. * \param searchExpression Search filter on projects name. - * \param flag If defined, it is used to filter out projects tagged as 'created' or 'shared' with a authorized user + * \param flag If defined, it is used to filter out projects tagged as 'created' or 'shared' with an authorized user * \param page Requested page of projects. * \returns unique id of a request */ - Q_INVOKABLE QString listProjects( const QString &searchExpression = QStringLiteral(), - const QString &flag = QStringLiteral(), - const int page = 1 ); + QString listProjects( const QString &searchExpression = QString(), + const QString &flag = QString(), + int page = 1 ); /** - * Sends non-blocking GET request to the server to listProjectsByName API. Response is handled in listProjectsByNameFinished - * method. Projects are parsed from response JSON. + * Sends non-blocking GET request to the server to listProjectsByName API. Response is handled in + * listProjectsByNameFinished method. Projects are parsed from response JSON. * * \param projectNames QStringList of project full names (namespace/name) - * \returns unique id of a sent request + * \returns unique id of sent request */ - Q_INVOKABLE QString listProjectsByName( const QStringList &projectNames = QStringList() ); + QString listProjectsByName( const QStringList &projectNames = QStringList() ); /** - * Sends non-blocking POST request to the server to pull (download) a project with a given name. On pullProjectReplyFinished, - * when a response is received, parses data-stream to files and rewrites local files with them. Extra files which don't match server - * files are removed. Emits syncProjectFinished at the end. + * Fallback method for listProjectsByName, which tries to request projects by IDs instead of names. + * Uses getProjectsDetails internally. + * \param projectIds QStringList of project IDs + * \see listProjectsByName + */ + void refetchBrokenProjects( const QStringList &projectIds ); + + /** + * Sends non-blocking POST request to the server to pull (download) a project with a given name. + * On pullProjectReplyFinished, when a response is received, parses data-stream to files and rewrites local files + * with them. Extra files which don't match server files are removed. Emits syncProjectFinished at the end. * If update has been successful, updates metadata file of the project. - * \param projectNamespace Project's namespace used in request. - * \param projectName Project's name used in request. + * \param projectFullName Project's "namespace/name" full name used for requests + * \param projectId Project's ID used for internal processing * \param withAuth If True, request is constructed with current authorization * \return true when sync has started, false otherwise (e.g. due to a missing authorization or invalid server) */ - Q_INVOKABLE bool pullProject( const QString &projectNamespace, const QString &projectName, bool withAuth = true ); + bool pullProject( const QString &projectFullName, const QString &projectId, bool withAuth = true ); /** * Sends non-blocking POST request to the server to push changes in a project with a given name. - * At the begining it checks if there are any changes on server and pulls them if so. - * If the pull was successful, it sends post request with list of local changes and modified/newly added files in JSON. + * At the beginning it checks if there are any changes on server and pulls them if so. If the pull was successful, + * it sends post request with list of local changes and modified/newly added files in JSON. * Emits syncProjectFinished at the end. - * \param projectNamespace Project's namespace used in request. - * \param projectName Project's name used in request. + * \param projectFullName Project's "namespace/name" full name used for requests + * \param projectId Project's ID used for internal processing * \param isInitialPush indicates if this is first push of the project (project creation) * \return true when sync has started, false otherwise (e.g. due to a missing authorization or invalid server) */ - Q_INVOKABLE bool pushProject( const QString &projectNamespace, const QString &projectName, bool isInitialPush = false ); + bool pushProject( const QString &projectFullName, const QString &projectId, bool isInitialPush = false ); /** * Sends non-blocking POST request to the server to cancel a running push of a project with a given name. - * If push has not started yet and a client is waiting for transaction UUID, it cancels the procedure just on client side - * without sending cancel request to the server. - * \param projectFullName Project's full name to cancel its 4 + * If push has not started yet and a client is waiting for transaction UUID, it cancels the procedure just + * on client side without sending cancel request to the server. + * \param projectId ID of project to cancel push for * \note pushCanceled() signal is emitted when the reply to the cancel request is received */ - Q_INVOKABLE void cancelPush( const QString &projectFullName ); + void cancelPush( const QString &projectId ); /** * Cancels pull either (1) before project data download starts or * (2) when data transfer has begun - connections are aborted. - * \param projectFullName Project's full name to cancel + * \param projectId Project's ID to cancel */ - Q_INVOKABLE void cancelPull( const QString &projectFullName ); + void cancelPull( const QString &projectId ); /** * Attempts to authorize user with the login and password @@ -337,8 +339,8 @@ class MerginApi: public QObject Q_INVOKABLE void getUserInfo(); Q_INVOKABLE void getWorkspaceInfo(); Q_INVOKABLE void getServiceInfo(); - Q_INVOKABLE void clearAuth(); - Q_INVOKABLE QString resetPasswordUrl(); + void clearAuth(); + Q_INVOKABLE QString resetPasswordUrl() const; /** * Registers new user. @@ -371,29 +373,22 @@ class MerginApi: public QObject */ Q_INVOKABLE void pingMergin(); - /** - * Uploads and registers a local project to Mergin. - * \param projectName Project name that will be migrated - * \param projectNamespace If empty, username of current user auth session is used. - */ - Q_INVOKABLE void migrateProjectToMergin( const QString &projectName, const QString &projectNamespace = QString() ); - /** * Makes a mergin project to be local by removing .mergin folder. Updates project's info and related models accordingly. - * \param projectNamespace Project namespace that will be detached from Mergin - * \param projectName Project name that will be detached from Mergin + * \param projectId Project id of project that will be detached from Mergin + * \param informUser Whether we notify user with notification about success */ - Q_INVOKABLE void detachProjectFromMergin( const QString &projectNamespace, const QString &projectName, bool informUser = true ); + void detachProjectFromMergin( const QString &projectId, bool informUser = true ); /** * Deletes all local projects and then tries to remove user account. */ Q_INVOKABLE void deleteAccount(); - static const int MERGIN_API_VERSION_MAJOR = 2020; - static const int MERGIN_API_VERSION_MINOR = 4; - static const int MINIMUM_SERVER_VERSION_MAJOR = 2023; - static const int MINIMUM_SERVER_VERSION_MINOR = 2; + static constexpr int MERGIN_API_VERSION_MAJOR = 2020; + static constexpr int MERGIN_API_VERSION_MINOR = 4; + static constexpr int MINIMUM_SERVER_VERSION_MAJOR = 2023; + static constexpr int MINIMUM_SERVER_VERSION_MINOR = 2; static const QString sMetadataFile; static const QString sMetadataFolder; static const QString sMerginConfigFile; @@ -404,9 +399,6 @@ class MerginApi: public QObject static bool isFileDiffable( const QString &fileName ) { return fileName.endsWith( ".gpkg" ); } - //! Get a list of all files that can be used with geodiff - QStringList projectDiffableFiles( const QString &projectFullName ); - static ProjectDiff localProjectChanges( const QString &projectDir ); static bool hasLocalProjectChanges( const QString &projectDir, bool supportsSelectiveSync ); @@ -419,37 +411,21 @@ class MerginApi: public QObject */ static bool parseVersion( const QString &version, int &major, int &minor ); - /** - * Finds project in merginProjects list according its full name. - * \param projectPath Full path to project's folder - * \param metadataFile Relative path of metafile to project's folder - */ - Q_INVOKABLE static QString getFullProjectName( QString projectNamespace, QString projectName ); - /** * Creates an empty project on Mergin server. isPublic determines if the new project will be visible to all or private - * \param projectNamespace - * \param projectName - * \param isPublic * \return true when project creation has started, false otherwise (e.g. due to a missing authorization) */ - bool createProject( const QString &projectNamespace, const QString &projectName, bool isPublic = false ); + bool createProject( const QString &projectNamespace, const QString &projectName, const QString &projectId, bool isPublic = false ); // Test functions /** - * Deletes the project of given namespace and name on Mergin server. - * Note that this deletion is not immediately done, - * but only scheduled to be deleted in few days. - * - * TODO - we should use DEL /v2/projects/ if possible, see - * TestMerginApi::deleteRemoteProjectNow() - * - * \param projectNamespace - * \param projectName + * Deletes the project of given projectId on Mergin server. + * \note This deletion is immediately done, if you want to schedule project for deletion use + * \code /v2/projects/{id}/scheduleDelete \endcode */ - void deleteProject( const QString &projectNamespace, const QString &projectName, bool informUser = true ); + void deleteProject( const QString &projectId, bool informUser = true ); - LocalProject getLocalProject( const QString &projectFullName ); + LocalProject getLocalProject( const QString &projectId ) const; // Production and Test functions (therefore not private) @@ -494,7 +470,7 @@ class MerginApi: public QObject const QList &oldServerFiles, const QList &localFiles, const QString &projectDir, - const MerginConfig config + const MerginConfig &config ); static QList getLocalProjectFiles( const QString &projectPath ); @@ -513,9 +489,9 @@ class MerginApi: public QObject /** * Performs checks and returns if a given file is excluded from the sync. - * If selective-sync-enabled is true, it checks if a file extension is from exlcudeSync extension list. - * If selective-sync-dir is defined, additionally checks, if the file is located in selective-sync-dir or in its subdir, - * otherwise a project dir is considered as selective-sync-dir and therefore the path check is redundant + * If selective-sync-enabled is true, it checks if a file extension is from excludeSync extension list. + * If selective-sync-dir is defined, additionally checks, if the file is located in selective-sync-dir or in its + * subdir, otherwise a project dir is considered as selective-sync-dir and therefore the path check is redundant * (since given filePath is relative to the project dir.). * @param filePath Relative path of a file to project directory. * @param config MerginConfig parsed from JSON, selective-sync properties are read from it. @@ -526,15 +502,6 @@ class MerginApi: public QObject bool apiSupportsSubscriptions() const; void setApiSupportsSubscriptions( bool apiSupportsSubscriptions ); - /** - * Sets projectNamespace and projectName from sourceString - url or any string from which takes last (name) - * and the previous of last (namespace) substring after splitting sourceString with slash. - * \param sourceString QString either url or fullname of a project - * \param projectNamespace QString to be set as namespace, might not change original value - * \param projectName QString to be set to name of a project - */ - static bool extractProjectName( const QString &sourceString, QString &projectNamespace, QString &projectName ); - bool supportsSelectiveSync() const; void setSupportsSelectiveSync( bool supportsSelectiveSync ); @@ -569,7 +536,7 @@ class MerginApi: public QObject void listInvitations(); /** - * Accepts or discards an invitaion to join workspace. + * Accepts or discards an invitation to join workspace. * \param uuid Invitation UUID * \param accept Whether user accepted invitation */ @@ -594,7 +561,7 @@ class MerginApi: public QObject /** * Returns true if server supports workspaces */ - bool apiSupportsWorkspaces(); + bool apiSupportsWorkspaces() const; /** * Returns true if the configured server has SSO enabled @@ -604,7 +571,7 @@ class MerginApi: public QObject /** * Reloads project metadata role by fetching latest information from server. */ - Q_INVOKABLE void reloadProjectRole( const QString &projectFullName ); + void reloadProjectRole( const QString &projectId ); /** * Returns the network manager used for Mergin API requests @@ -629,20 +596,19 @@ class MerginApi: public QObject void listProjectsFinished( const MerginProjectsList &merginProjects, int projectCount, int page, QString requestId ); void listProjectsFailed(); void listProjectsByNameFinished( const MerginProjectsList &merginProjects, QString requestId ); - void syncProjectFinished( const QString &projectFullName, bool successfully, int version ); - void projectReloadNeededAfterSync( const QString &projectFullName ); + void syncProjectFinished( const QString &projectId, bool successfully, int version ); + void projectReloadNeededAfterSync( const QString &projectId ); /** * Emitted when sync starts/finishes or the progress changes - useful to give a clue in the GUI about the status. * Normally progress is in interval [0, 1] as data get pushed or pulled. * With no pending sync, progress is set to -1 */ - void syncProjectStatusChanged( const QString &projectFullName, qreal progress ); + void syncProjectStatusChanged( const QString &projectId, qreal progress ); void networkErrorOccurred( const QString &message, - const QString &topic, int httpCode = -1, - const QString &projectFullName = QLatin1String() + const QString &projectId = QString() ); void storageLimitReached( qreal uploadSize ); @@ -662,8 +628,7 @@ class MerginApi: public QObject void postRegistrationFailed( const QString &msg ); void apiRootChanged(); void apiVersionStatusChanged(); - void projectCreated( const QString &projectFullName, bool result ); - void serverProjectDeleted( const QString &projecFullName, bool result ); + void projectCreated( const QString &projectId, bool result ); void userInfoChanged(); void workspaceInfoChanged(); void subscriptionInfoChanged(); @@ -672,13 +637,14 @@ class MerginApi: public QObject void pingMerginFinished( const QString &apiVersion, bool serverSupportsSubscriptions, const QString &msg ); void pullFilesStarted(); void pushFilesStarted(); - void pushCanceled( const QString &projectFullName, bool result ); - void projectDataChanged( const QString &projectFullName ); - void projectDetached( const QString &projectFullName ); - void projectAttachedToMergin( const QString &projectFullName, const QString &previousProjectName ); - - void projectAlreadyOnLatestVersion( const QString &projectFullName ); - void missingAuthorizationError( const QString &projectFullName ); + void pushCanceled( const QString &projectId ); + void projectDataChanged( const QString &projectFullName, const QString &projectId ); + void projectDetached( const QString &projectId ); + void projectAttachedToMergin( const QString &projectId ); + void refetchBrokenProjectsFinished( const MerginProjectsList &projectList ); + + void projectAlreadyOnLatestVersion( const QString &projectId ); + void missingAuthorizationError( const QString &projectId ); void accountDeleted( bool result ); void userIsAnOrgOwnerError(); @@ -702,11 +668,13 @@ class MerginApi: public QObject void serverWasUpgraded(); - void projectRoleUpdated( const QString &projectFullName, const QString &role ); + void projectRoleUpdated( const QString &projectId, const QString &role ); void networkManagerChanged(); - void downloadItemRetried( const QString &projectFullName, int retryCount ); + void downloadItemRetried( const QString &projectId, int retryCount ); + + void projectIdChanged( const QString &projectFullName, QString projectId ); void apiSupportsSsoChanged(); @@ -714,12 +682,14 @@ class MerginApi: public QObject void ssoConfigIsMultiTenant(); private slots: - void listProjectsReplyFinished( QString requestId ); - void listProjectsByNameReplyFinished( QString requestId ); + void listProjectsReplyFinished( const QString &requestId ); + void listProjectsByNameReplyFinished( const QString &requestId ); + void getProjectsDetailsReplyFinished(); + void refetchBrokenProjectsReplyFinished(); // Pull slots void pullInfoReplyFinished(); - void downloadItemReplyFinished( DownloadQueueItem item ); + void downloadItemReplyFinished( const DownloadQueueItem &item ); void cacheServerConfig(); // Push slots @@ -732,11 +702,11 @@ class MerginApi: public QObject void getUserInfoFinished(); void getWorkspaceInfoReplyFinished(); void getServiceInfoReplyFinished(); - void saveAuthData(); + void saveAuthData() const; void createProjectFinished(); void deleteProjectFinished( bool informUser = true ); void authorizeFinished(); - void registrationFinished( const QString &login = QStringLiteral(), const QString &password = QStringLiteral() ); + void registrationFinished( const QString &login = QString(), const QString &password = QString() ); void postRegistrationFinished(); void pingMerginReplyFinished(); void deleteAccountFinished(); @@ -762,32 +732,34 @@ class MerginApi: public QObject static QStringList generateChunkIdsForSize( qint64 fileSize ); QJsonArray prepareUploadChangesJSON( const QList &files ); static QString getApiKey( const QString &serverName ); - void abortPullItems( const QString &projectFullName ); + void abortPullItems( const QString &projectId ); /** * Sends non-blocking POST request to the server to upload a file (chunk). * \param projectFullName Namespace/name + * \param projectId ID of project * \param json project info containing metadata for upload */ - void pushStart( const QString &projectFullName, const QByteArray &json ); + void pushStart( const QString &projectFullName, const QString &projectId, const QByteArray &json ); /** * Sends non-blocking POST request to the server to upload a file (chunk). * \param projectFullName Namespace/name + * \param projectId ID of project * \param transactionUUID Transaction ID which servers sends on uploadStart * \param file Mergin file to upload * \param chunkNo Chunk number of given file to be uploaded */ - void pushFile( const QString &projectFullName, const QString &transactionUUID, MerginFile file, int chunkNo = 0 ); + void pushFile( const QString &projectFullName, const QString &projectId, const QString &transactionUUID, const MerginFile &file, int chunkNo = 0 ); /** * Closing request after successful push. * \param projectFullName Namespace/name + * \param projectId ID of project * \param transactionUUID transaction UUID to match upload process on the server */ - void pushFinish( const QString &projectFullName, const QString &transactionUUID ); - - void sendPushCancelRequest( const QString &projectFullName, const QString &transactionUUID ); + void pushFinish( const QString &projectFullName, const QString &projectId, const QString &transactionUUID ); + void sendPushCancelRequest( const QString &projectFullName, const QString &projectId, const QString &transactionUUID ); bool writeData( const QByteArray &data, const QString &path ); void createPathIfNotExists( const QString &filePath ); @@ -795,21 +767,21 @@ class MerginApi: public QObject static QSet listFiles( const QString &projectPath ); bool validateAuth(); - void checkMerginVersion( QString apiVersion, bool serverSupportsSubscriptions, QString msg = QStringLiteral() ); + void checkMerginVersion( const QString &apiVersion, bool serverSupportsSubscriptions, const QString &msg = QStringLiteral() ); /** - * Extracts string code of an error json. If its not json or value cannot be parsed, QString() is return; + * Extracts string code of an error json. If it's not json or value cannot be parsed, QString() is return; * \param data Data received from mergin server on a request failed. */ QString extractServerErrorCode( const QByteArray &data ); /** - * Extracts value of an error json. If its not json or value cannot be parsed, QVariant() is return; + * Extracts value of an error json. If it's not json or value cannot be parsed, QVariant() is return; * \param data Data received from mergin server on a request failed. * \param key Where should be a value from data */ QVariant extractServerErrorValue( const QByteArray &data, const QString &key ); /** - * Extracts detail (message) of an error json. If its not json or detail cannot be parsed, the whole data are return; + * Extracts detail (message) of an error json. If it's not json or detail cannot be parsed, the whole data are return; * \param data Data received from mergin server on a request failed. */ QString extractServerErrorMsg( const QByteArray &data ); @@ -817,34 +789,59 @@ class MerginApi: public QObject * Returns a temporary project path. * \param projectFullName */ - QString getTempProjectDir( const QString &projectFullName ); + QString getTempProjectDir( const QString &projectFullName ) const; - /** Creates a request to get project details (list of project files). + /** + * Creates a request to get project details with list of project files. The reason why we use both variants is + * the limitation of API, currently the endpoint which uses project ID doesn't return necessary versioning + * information for synchronization workflow. Thus, for synchronization use this method and for basic project details + * use \a getProjectDetails. + * \param projectFullName Project full name to use for request + * \param projectId project ID + * \param withAuth Specifies if the request will have authentication token + * \see getProjectDetails, getProjectsDetails */ - QNetworkReply *getProjectInfo( const QString &projectFullName, bool withAuth = true ); + QNetworkReply *getProjectInfo( const QString &projectFullName, const QString &projectId, bool withAuth = true ); - //! Called when pull of project data has finished to finalize things and emit sync finished signal - void finalizeProjectPull( const QString &projectFullName ); + /** + * Creates a request to get project details. The reason why we use both variants is + * the limitation of API, currently the endpoint which uses project ID doesn't return necessary versioning + * information for synchronization workflow. Thus, for synchronization use \a getProjectInfo. + * \param projectId project ID to use for request + * \param withAuth Specifies if the request will have authentication token + * \see getProjectInfo, getProjectsDetails + */ + QNetworkReply *getProjectDetails( const QString &projectId, bool withAuth = true ); + /** + * Similar to getProjectDetails, but it can fetch multiple projects in one call. Also, we use it as a fallback for + * getProjectInfo during syncing. The reason we use this instead of getProjectDetails is that this response is + * lighter and doesn't include file history. + * \param projectIds project IDs to use for request + * \param withAuth Specifies if the request will have authentication token + * \see getProjectInfo, getProjectDetails + */ + QNetworkReply *getProjectsDetails( const QStringList &projectIds, bool withAuth = true ); + + //! Called when pull of project data has finished to finalize things and emit sync finished signal + void finalizeProjectPull( const QString &projectId ); void finalizeProjectPullCopy( const QString &projectFullName, const QString &projectDir, const QString &tempDir, const QString &filePath, const QList &items ); - bool finalizeProjectPullApplyDiff( const QString &projectFullName, const QString &projectDir, const QString &tempDir, const QString &filePath, const QList &items ); + bool finalizeProjectPullApplyDiff( const QString &projectFullName, const QString &projectId, const QString &projectDir, const QString &tempDir, const QString &filePath, const QList &items ); //! Takes care of removal of the transaction, writing new metadata and emits syncProjectFinished() - void finishProjectSync( const QString &projectFullName, bool syncSuccessful ); - - void prepareProjectPull( const QString &projectFullName, const QByteArray &data ); - - void startProjectPull( const QString &projectFullName ); + void finishProjectSync( const QString &projectFullName, const QString &projectId, bool syncSuccessful ); + void prepareProjectPull( const QString &projectId, const QByteArray &data ); + void startProjectPull( const QString &projectId ); //! Takes care of finding the correct config file, appends it to current transaction and proceeds with project pull - void prepareDownloadConfig( const QString &projectFullName, bool downloaded = false ); - void requestServerConfig( const QString &projectFullName ); + void prepareDownloadConfig( const QString &projectId, bool downloaded = false ); + void requestServerConfig( const QString &projectId ); //! Starts download request of another item - void downloadNextItem( const QString &projectFullName ); + void downloadNextItem( const QString &projectId ); //! Removes temp folder for project - void removeProjectsTempFolder( const QString &projectNamespace, const QString &projectName ); + void removeProjectsTempFolder( const QString &projectNamespace, const QString &projectName ) const; //! Refreshes auth token if it is expired. It does a blocking call to authorize. //! Works only when login, password and token is set in UserAuth. Does nothing if using SSO. @@ -855,9 +852,9 @@ class MerginApi: public QObject * \param reply Network reply to check for retryable errors * \returns True if the error should trigger a retry, false otherwise */ - bool isRetryableNetworkError( QNetworkReply *reply ); + bool isRetryableNetworkError( const QNetworkReply *reply ); - QNetworkRequest getDefaultRequest( bool withAuth = true ); + QNetworkRequest getDefaultRequest( bool withAuth = true ) const; bool projectFileHasBeenUpdated( const ProjectDiff &diff ); @@ -866,10 +863,10 @@ class MerginApi: public QObject void reloadProjectRoleReplyFinished(); //! Updates project role in metadata file - bool updateCachedProjectRole( const QString &projectFullName, const QString &newRole ); + bool updateCachedProjectRole( const QString &projectId, const QString &newRole ) const; //! Retrieves cached role from metadata file - QString getCachedProjectRole( const QString &projectFullName ) const; + QString getCachedProjectRole( const QString &projectId ) const; void startSsoFlow( const QString &clientId ); @@ -889,10 +886,12 @@ class MerginApi: public QObject AttrProjectFullName = QNetworkRequest::User, AttrTempFileName = QNetworkRequest::User + 1, AttrWorkspaceName = QNetworkRequest::User + 2, - AttrAcceptFlag = QNetworkRequest::User + 3 + AttrAcceptFlag = QNetworkRequest::User + 3, + AttrProjectId = QNetworkRequest::User + 4, + AttrAuthUsed = QNetworkRequest::User + 5 }; - Transactions mTransactionalStatus; //projectFullname -> transactionStatus + Transactions mTransactionalStatus; //projectId -> transactionStatus static const QSet sIgnoreExtensions; static const QSet sIgnoreImageExtensions; static const QSet sIgnoreFiles; diff --git a/core/merginprojectmetadata.h b/core/merginprojectmetadata.h index 00705add7..f6ea2130f 100644 --- a/core/merginprojectmetadata.h +++ b/core/merginprojectmetadata.h @@ -10,8 +10,6 @@ #ifndef MERGINPROJECTMETADATA_H #define MERGINPROJECTMETADATA_H -#include -#include #include struct MerginFile @@ -36,7 +34,7 @@ struct MerginFile // (could be multiple diffs that need to be applied sequentially) // - bool pullCanUseDiff = false; //!< whether or not we can update the local file by downloading and applying diffs + bool pullCanUseDiff = false; //!< whether we can update the local file by downloading and applying diffs QList< QPair > pullDiffFiles; //!< list of diffs that will need to be fetched: the version and their sizes static MerginFile fromJsonObject( const QJsonObject &merginFileInfo ); diff --git a/core/merginprojectstatusmodel.cpp b/core/merginprojectstatusmodel.cpp index ee8861c71..d32232fd8 100644 --- a/core/merginprojectstatusmodel.cpp +++ b/core/merginprojectstatusmodel.cpp @@ -36,11 +36,11 @@ QHash MerginProjectStatusModel::roleNames() const return roleNames; } -QVariant MerginProjectStatusModel::data( const QModelIndex &index, int role ) const +QVariant MerginProjectStatusModel::data( const QModelIndex &index, const int role ) const { - int row = index.row(); + const int row = index.row(); if ( row < 0 || row >= mItems.count() ) - return QVariant(); + return {}; ProjectStatusItem item = mItems.at( row ); @@ -54,7 +54,7 @@ QVariant MerginProjectStatusModel::data( const QModelIndex &index, int role ) co case Updates: return item.updates; case Section: return item.section; } - return QVariant(); + return {}; } @@ -123,14 +123,14 @@ void MerginProjectStatusModel::infoProjectUpdated( const ProjectDiff &projectDif endResetModel(); } -bool MerginProjectStatusModel::loadProjectInfo( const QString &projectFullName ) +bool MerginProjectStatusModel::loadProjectInfo( const QString &projectId ) { - LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); + const LocalProject projectInfo = mLocalProjects.projectFromProjectId( projectId ); if ( !projectInfo.projectDir.isEmpty() ) { - ProjectDiff diff = MerginApi::localProjectChanges( projectInfo.projectDir ); + const ProjectDiff diff = MerginApi::localProjectChanges( projectInfo.projectDir ); - bool hasLocalChanges = !diff.localAdded.isEmpty() || !diff.localUpdated.isEmpty() || !diff.localDeleted.isEmpty(); + const bool hasLocalChanges = !diff.localAdded.isEmpty() || !diff.localUpdated.isEmpty() || !diff.localDeleted.isEmpty(); if ( hasLocalChanges ) infoProjectUpdated( diff, projectInfo.projectDir ); diff --git a/core/merginprojectstatusmodel.h b/core/merginprojectstatusmodel.h index fa8d988b0..59a6d3a9c 100644 --- a/core/merginprojectstatusmodel.h +++ b/core/merginprojectstatusmodel.h @@ -10,7 +10,6 @@ #ifndef MERGINPROJECTSTATUSMODEL_H #define MERGINPROJECTSTATUSMODEL_H -#include #include #include "merginapi.h" @@ -59,7 +58,7 @@ class MerginProjectStatusModel : public QAbstractListModel QHash roleNames() const override; Q_INVOKABLE QVariant data( const QModelIndex &index, int role ) const override; - Q_INVOKABLE bool loadProjectInfo( const QString &projectFullName ); + Q_INVOKABLE bool loadProjectInfo( const QString &projectId ); private: void insertIntoItems( const QSet &files, const ProjectChangelogStatus &status, const QString &projectDir ); diff --git a/core/project.cpp b/core/project.cpp index 046cc75f8..ef7b4c87c 100644 --- a/core/project.cpp +++ b/core/project.cpp @@ -13,24 +13,41 @@ QString LocalProject::id() const { - return fullName(); + // if project directory or project name doesn't exist return empty string + if ( projectDir.isEmpty() || projectName.isEmpty() ) + return {}; + + return projectId; } QString LocalProject::fullName() const { if ( !projectName.isEmpty() && !projectNamespace.isEmpty() ) - return MerginApi::getFullProjectName( projectNamespace, projectName ); + return CoreUtils::getFullProjectName( projectNamespace, projectName ); if ( projectDir.isEmpty() ) - return QString(); + return {}; - QDir dir( projectDir ); + const QDir dir( projectDir ); return dir.dirName(); } +QString LocalProject::generateProjectId() +{ + return CoreUtils::uuidWithoutBraces( QUuid::createUuid() ); +} + QString MerginProject::id() const { - return MerginApi::getFullProjectName( projectNamespace, projectName ); + return projectId; +} + +QString MerginProject::fullName() const +{ + if ( !projectName.isEmpty() && !projectNamespace.isEmpty() ) + return CoreUtils::getFullProjectName( projectNamespace, projectName ); + + return {}; } ProjectStatus::Status ProjectStatus::projectStatus( const Project &project, const bool supportsSelectiveSync ) @@ -58,9 +75,9 @@ ProjectStatus::Status ProjectStatus::projectStatus( const Project &project, cons return ProjectStatus::UpToDate; } -bool ProjectStatus::hasLocalChanges( const LocalProject &project, bool supportsSelectiveSync ) +bool ProjectStatus::hasLocalChanges( const LocalProject &project, const bool supportsSelectiveSync ) { - QString metadataFilePath = project.projectDir + "/" + MerginApi::sMetadataFile; + const QString metadataFilePath = project.projectDir + "/" + MerginApi::sMetadataFile; // If the project does not have metadata file, there are local changes if ( !QFile::exists( metadataFilePath ) ) diff --git a/core/project.h b/core/project.h index 6e1879078..9a52076fe 100644 --- a/core/project.h +++ b/core/project.h @@ -10,8 +10,6 @@ #ifndef PROJECT_H #define PROJECT_H -#include -#include #include #include @@ -37,10 +35,8 @@ namespace ProjectStatus /** * \brief The LocalProject struct is used as a struct for projects that are available on the device. - * The struct is used in the \see Projects struct and also for communication between LocalProjectsManager and ProjectsModel - * - * \note Struct contains member id() which in this time returns projects full name, however, once we - * start using projects IDs, it can be replaced for that ID. + * The struct is used in the \see Projects struct and also for communication between LocalProjectsManager + * and ProjectsModel. */ struct LocalProject { @@ -48,15 +44,22 @@ struct LocalProject public: + // TODO: remove? Q_PROPERTY( QString qgisProjectFilePath MEMBER qgisProjectFilePath ) - LocalProject() {}; - ~LocalProject() {}; + LocalProject() = default; + ~LocalProject() = default; QString projectName; QString projectNamespace; + QString projectId; + + //! Returns the UUID of project + Q_INVOKABLE QString id() const; + + //! generates a new UUID + static QString generateProjectId(); - Q_INVOKABLE QString id() const; //! projectFullName for time being QString fullName() const; QString projectDir; @@ -68,18 +71,20 @@ struct LocalProject bool isValid() const { return !projectDir.isEmpty(); } - //! Returns true if the local version instance has a mergin counterpart based on localVersion. - //! LocalVersion comes from metadata file stored in .mergin folder. - //! Note: this is just for scenarios where you only have LocalProject instance and not Project, - //! Project->isMergin() is recommended to use over this one + /** + * Returns true if the local version instance has a mergin counterpart based on localVersion. + * LocalVersion comes from metadata file stored in .mergin folder. + * Note: this is just for scenarios where you only have LocalProject instance and not Project, + * \note Project->isMergin() is recommended to use over this one + */ bool hasMerginMetadata() const { return localVersion > -1; } - bool operator ==( const LocalProject &other ) + bool operator ==( const LocalProject &other ) const { return ( this->id() == other.id() ); } - bool operator !=( const LocalProject &other ) + bool operator !=( const LocalProject &other ) const { return !( *this == other ); } @@ -88,19 +93,21 @@ struct LocalProject /** * \brief The MerginProject struct is used for projects that comes from Mergin. * This struct is used in the \see Projects struct and also for communication between MerginAPI and ProjectsModel - * - * \note Struct contains member id() which in this time returns projects full name, however, once we - * start using projects IDs, it can be replaced for that ID. */ struct MerginProject { - MerginProject() {}; - ~MerginProject() {}; + MerginProject() = default; + ~MerginProject() = default; QString projectName; QString projectNamespace; + QString projectId; - QString id() const; //!< projectFullName for time being + /** + * Returns the project ID or empty string if no ID is known. Then it's necessary to fetch the ID from API. + */ + QString id() const; + QString fullName() const; QDateTime serverUpdated; // available latest version of project files on server int serverVersion = -1; @@ -111,12 +118,12 @@ struct MerginProject bool isValid() const { return !projectName.isEmpty() && !projectNamespace.isEmpty(); } - bool operator ==( const MerginProject &other ) + bool operator ==( const MerginProject &other ) const { return ( this->id() == other.id() ); } - bool operator !=( const MerginProject &other ) + bool operator !=( const MerginProject &other ) const { return !( *this == other ); } @@ -125,13 +132,13 @@ struct MerginProject /** * \brief The Project struct serves as a struct for any kind of project (local/mergin). * It consists of two main parts - mergin and local. - * Both parts are pointers to their specific structs and based on the pointer value (nullptr or assigned) this structs - * decides if the project is local, mergin or both. + * Both parts are pointers to their specific structs and based on the pointer value (nullptr or assigned) these structs + * decide if the project is local, mergin or both. */ struct Project { - Project() {}; - ~Project() {}; + Project() = default; + ~Project() = default; MerginProject mergin; LocalProject local; @@ -142,50 +149,52 @@ struct Project QString projectName() const { if ( isMergin() ) return mergin.projectName; - else if ( isLocal() ) return local.projectName; - return QString(); + if ( isLocal() ) return local.projectName; + return {}; } QString projectNamespace() const { if ( isMergin() ) return mergin.projectNamespace; - else if ( isLocal() ) return local.projectNamespace; - return QString(); + if ( isLocal() ) return local.projectNamespace; + return {}; } QString id() const { if ( isMergin() ) return mergin.id(); - else if ( isLocal() ) return local.id(); - return QString(); + if ( isLocal() ) return local.id(); + return {}; } QString fullName() const { - return id(); + if ( isMergin() ) return mergin.fullName(); + if ( isLocal() ) return local.fullName(); + return {}; } - bool operator ==( const Project &other ) + bool operator ==( const Project &other ) const { if ( this->isLocal() && other.isLocal() ) { - return this->local.id() == other.local.id(); + return this->local.id() == other.local.id() && this->local.fullName() == other.local.fullName(); } - else if ( this->isMergin() && other.isMergin() ) + if ( this->isMergin() && other.isMergin() ) { - return this->mergin.id() == other.mergin.id(); + return this->mergin.id() == other.mergin.id() && this->mergin.fullName() == other.mergin.fullName(); } return false; } - bool operator !=( const Project &other ) + bool operator !=( const Project &other ) const { return !( *this == other ); } }; typedef QList MerginProjectsList; -typedef QList LocalProjectsList; +typedef QHash LocalProjectsDict; Q_DECLARE_METATYPE( MerginProjectsList ) #endif // PROJECT_H