From 30abf5b20061c1f2b2a76c6f2e6292836d2f5335 Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Sat, 26 Sep 2026 00:38:59 +0200 Subject: [PATCH 1/5] Decouple selective sync configuration --- app/projectsmodel.cpp | 18 +-- core/localprojectsmanager.cpp | 2 +- core/merginapi.cpp | 221 +++++++++++++++++------------- core/merginapi.h | 45 +++--- core/merginprojectmetadata.cpp | 19 ++- core/merginprojectmetadata.h | 12 +- core/merginprojectstatusmodel.cpp | 2 +- core/project.cpp | 4 +- core/project.h | 2 +- core/synchronizationmanager.cpp | 4 +- 10 files changed, 179 insertions(+), 150 deletions(-) diff --git a/app/projectsmodel.cpp b/app/projectsmodel.cpp index bb3bbea8e..71b6e62a2 100644 --- a/app/projectsmodel.cpp +++ b/app/projectsmodel.cpp @@ -95,7 +95,7 @@ QVariant ProjectsModel::data( const QModelIndex &index, int role ) const } else { - ProjectStatus::Status status = ProjectStatus::projectStatus( project, *mBackend ); + ProjectStatus::Status status = ProjectStatus::projectStatus( project ); if ( status == ProjectStatus::NeedsSync ) { @@ -285,7 +285,7 @@ void ProjectsModel::mergeProjects( const MerginProjectsList &merginProjects, Mer if ( res != merginProjects.end() ) { project.mergin = *res; - project.mergin.status = ProjectStatus::projectStatus( project, *mBackend ); + project.mergin.status = ProjectStatus::projectStatus( project ); } else if ( project.local.hasMerginMetadata() ) { @@ -295,7 +295,7 @@ void ProjectsModel::mergeProjects( const MerginProjectsList &merginProjects, Mer // (listProjectsByName API limits response to max 50 projects) project.mergin.projectName = project.local.projectName; project.mergin.projectNamespace = project.local.projectNamespace; - project.mergin.status = ProjectStatus::projectStatus( project, *mBackend ); + project.mergin.status = ProjectStatus::projectStatus( project ); } mProjects << project; @@ -317,7 +317,7 @@ void ProjectsModel::mergeProjects( const MerginProjectsList &merginProjects, Mer Project project; MerginApi::extractProjectName( pendingProjectName, project.mergin.projectNamespace, project.mergin.projectName ); - project.mergin.status = ProjectStatus::projectStatus( project, *mBackend ); + project.mergin.status = ProjectStatus::projectStatus( project ); mProjects << project; } @@ -340,7 +340,7 @@ void ProjectsModel::mergeProjects( const MerginProjectsList &merginProjects, Mer { project.local = *match; } - project.mergin.status = ProjectStatus::projectStatus( project, *mBackend ); + project.mergin.status = ProjectStatus::projectStatus( project ); mProjects << project; } @@ -429,7 +429,7 @@ void ProjectsModel::onProjectSyncFinished( const QString &projectFullName, bool project.mergin.serverVersion = newVersion; } - project.mergin.status = ProjectStatus::projectStatus( project, *mBackend ); + project.mergin.status = ProjectStatus::projectStatus( project ); QModelIndex changeIndex = index( ix ); emit dataChanged( changeIndex, changeIndex, { ProjectSyncPending, ProjectSyncProgress, ProjectStatus } ); @@ -474,7 +474,7 @@ void ProjectsModel::onProjectAdded( const LocalProject &localProject ) project.local = localProject; if ( project.isMergin() ) { - project.mergin.status = ProjectStatus::projectStatus( project, *mBackend ); + project.mergin.status = ProjectStatus::projectStatus( project ); } QModelIndex modelIx = index( ix ); @@ -510,7 +510,7 @@ void ProjectsModel::onAboutToRemoveProject( const LocalProject &localProject ) { // just remove local part mProjects[ix].local = LocalProject(); - mProjects[ix].mergin.status = ProjectStatus::projectStatus( mProjects[ix], *mBackend ); + mProjects[ix].mergin.status = ProjectStatus::projectStatus( mProjects[ix] ); QModelIndex modelIx = index( ix ); emit dataChanged( modelIx, modelIx ); @@ -531,7 +531,7 @@ void ProjectsModel::onProjectDataChanged( const LocalProject &localProject ) if ( project.isMergin() ) { - project.mergin.status = ProjectStatus::projectStatus( project, *mBackend ); + project.mergin.status = ProjectStatus::projectStatus( project ); } QModelIndex editIndex = index( ix ); diff --git a/core/localprojectsmanager.cpp b/core/localprojectsmanager.cpp index d7767cbf3..693e63055 100644 --- a/core/localprojectsmanager.cpp +++ b/core/localprojectsmanager.cpp @@ -165,7 +165,7 @@ QString LocalProjectsManager::projectChanges( const QString &projectId ) if ( project.isValid() ) { - return MerginApi::localProjectChanges( project.projectDir ).dump(); + return MerginApi::localChanges( project.projectDir ).dump(); } return QString(); diff --git a/core/merginapi.cpp b/core/merginapi.cpp index 1a6fc70ef..09610de72 100644 --- a/core/merginapi.cpp +++ b/core/merginapi.cpp @@ -37,7 +37,8 @@ const QString MerginApi::sMetadataFile = QStringLiteral( "/.mergin/mergin.json" ); const QString MerginApi::sMetadataFolder = QStringLiteral( ".mergin" ); -const QString MerginApi::sMerginConfigFile = QStringLiteral( "mergin-config.json" ); +const QString MerginApi::sSelectiveSyncConfigFile = QStringLiteral( "mergin-config.json" ); +const QString MerginApi::sIgnoreSelectiveSyncFileFlag = QStringLiteral( ".mergin/selective-sync.ignore" ); const QString MerginApi::sDefaultApiRoot = QStringLiteral( "https://app.merginmaps.com" ); const QString MerginApi::sDefaultReportLogUrl = QStringLiteral( "https://g4pfq226j0.execute-api.eu-west-1.amazonaws.com/mergin_client_log_submit" ); const QSet MerginApi::sIgnoreExtensions = QSet() << "gpkg-shm" << "gpkg-wal" << "qgs~" << "qgz~" << "pyc" << "swap"; @@ -206,7 +207,6 @@ bool MerginApi::pushProject( const QString &projectNamespace, const QString &pro TransactionStatus transaction; transaction.isInitialPush = isInitialPush; - transaction.configAllowed = mSupportsSelectiveSync; transaction.type = TransactionStatus::Push; if ( mPushVersion == MerginServerType::syncTransactionVersion::v1 ) @@ -331,10 +331,15 @@ void MerginApi::preparePushPayload( const QString &projectFullName ) transaction.projectId = oldServerProject.id; CoreUtils::log( "push " + projectFullName, "Project ID: " + transaction.projectId ); - // Cache mergin-config, since we are on the most recent version, it is sufficient to just read the local version - if ( transaction.configAllowed ) + // Check if selective sync is ignored or not in this project + bool selectiveSyncIgnored = QFileInfo::exists( transaction.projectDir + "/" + sIgnoreSelectiveSyncFileFlag ); + if ( selectiveSyncIgnored ) { - transaction.config = MerginConfig::fromFile( transaction.projectDir + "/" + MerginApi::sMerginConfigFile ); + transaction.ignoreSelectiveSync = true; + } + else + { + transaction.selectiveSync = SelectiveSyncConfig::fromFile( transaction.projectDir + "/" + MerginApi::sSelectiveSyncConfigFile ); } // Calculate local changes @@ -343,15 +348,15 @@ void MerginApi::preparePushPayload( const QString &projectFullName ) oldServerProject.files, localFiles, transaction.projectDir, - transaction.configAllowed, - transaction.config + !transaction.ignoreSelectiveSync, + transaction.selectiveSync ); CoreUtils::log( "push " + projectFullName, localDiff.dump() ); - if ( transaction.configAllowed && transaction.config.isValid && transaction.config.selectiveSyncEnabled ) + if ( !transaction.ignoreSelectiveSync && transaction.selectiveSync.isValid && transaction.selectiveSync.enabled ) { - const QString sDir = transaction.config.selectiveSyncDir.isEmpty() ? QStringLiteral( " for entire project" ) : QStringLiteral( ", on path: %1" ).arg( transaction.config.selectiveSyncDir ); + const QString sDir = transaction.selectiveSync.dir.isEmpty() ? QStringLiteral( " for entire project" ) : QStringLiteral( ", on path: %1" ).arg( transaction.selectiveSync.dir ); CoreUtils::log( "push " + projectFullName, QStringLiteral( "Selective sync is enabled%1" ).arg( sDir ) ); } else @@ -404,7 +409,7 @@ void MerginApi::preparePushPayload( const QString &projectFullName ) file.chunks = generateChunkIdsForSize( file.size ); - if ( mSupportsSelectiveSync && *filePath == sMerginConfigFile ) + if ( !transaction.ignoreSelectiveSync && *filePath == sSelectiveSyncConfigFile ) { // problem, we changed selective sync config locally, this is undefined and sync must stop CoreUtils::log( "push " + projectFullName, QStringLiteral( "PROBLEM! Selective sync config was added locally, this is undefined and might lead to data loss, aborting push..." ) ); @@ -483,7 +488,7 @@ void MerginApi::preparePushPayload( const QString &projectFullName ) } } - if ( mSupportsSelectiveSync && *filePath == sMerginConfigFile ) + if ( !transaction.ignoreSelectiveSync && *filePath == sSelectiveSyncConfigFile ) { // problem, we changed selective sync config locally, this is undefined and sync must stop CoreUtils::log( "push " + projectFullName, QStringLiteral( "PROBLEM! Selective sync config was updated locally, this is undefined and might lead to data loss, aborting push..." ) ); @@ -505,7 +510,7 @@ void MerginApi::preparePushPayload( const QString &projectFullName ) if ( transaction.pushChanges.added.isEmpty() && transaction.pushChanges.updated.isEmpty() && transaction.pushChanges.removed.isEmpty() ) { - finishTransaction( projectFullName, true ); + finishTransaction( projectFullName, false ); // -- false here intentionally so that stamp does not get overwritten! return; } @@ -760,16 +765,6 @@ bool MerginApi::authConfigurationHasChanged( const ProjectDiff &diff ) return false; } -bool MerginApi::supportsSelectiveSync() const -{ - return mSupportsSelectiveSync; -} - -void MerginApi::setSupportsSelectiveSync( bool supportsSelectiveSync ) -{ - mSupportsSelectiveSync = supportsSelectiveSync; -} - bool MerginApi::apiSupportsSubscriptions() const { return mApiSupportsSubscriptions; @@ -917,7 +912,7 @@ void MerginApi::abortPullItems( const QString &projectFullName ) finishTransaction( projectFullName, false ); } -void MerginApi::cacheServerConfig() +void MerginApi::cacheSelectiveSyncConfig() { QNetworkReply *r = qobject_cast( sender() ); Q_ASSERT( r ); @@ -932,13 +927,13 @@ void MerginApi::cacheServerConfig() { QByteArray data = r->readAll(); - CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Downloaded mergin config (%1 bytes)" ).arg( data.size() ) ); - transaction.config = MerginConfig::fromJson( data ); + CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Downloaded selective sync config (%1 bytes)" ).arg( data.size() ) ); + transaction.selectiveSync = SelectiveSyncConfig::fromJson( data ); transaction.replyPullServerConfig->deleteLater(); transaction.replyPullServerConfig = nullptr; - prepareDownloadConfig( projectFullName, true ); + prepareSelectiveSyncConfig( projectFullName, true ); } else { @@ -948,7 +943,7 @@ void MerginApi::cacheServerConfig() { serverMsg = r->errorString(); } - CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Failed to cache mergin config - %1. %2" ).arg( r->errorString(), serverMsg ) ); + CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Failed to cache selective sync config - %1. %2" ).arg( r->errorString(), serverMsg ) ); transaction.replyPullServerConfig->deleteLater(); transaction.replyPullServerConfig = nullptr; @@ -1023,7 +1018,7 @@ void MerginApi::pushFile( const QString &projectFullName, MerginFile file, int c Q_ASSERT( !transaction.replyPushFile ); transaction.replyPushFile = mManager->post( request, data ); - CoreUtils::log( "push " + projectFullName, QStringLiteral( "Uploading file: %1, chunk: %2/%3" ).arg( file.path ).arg( chunkNo ).arg( file.chunks.count() ) ); + CoreUtils::log( "push " + projectFullName, QStringLiteral( "Uploading file: %1, chunk: %2/%3" ).arg( file.path ).arg( chunkNo + 1 ).arg( file.chunks.count() ) ); if ( mPushVersion == MerginServerType::syncTransactionVersion::v2 ) { @@ -1295,7 +1290,7 @@ void MerginApi::pushV2Finish( const QString &projectFullName ) CoreUtils::log( "push " + projectFullName, QStringLiteral( "Requesting to create a new project version: %1" ).arg( transaction.version ) ); } -bool MerginApi::pullProject( const QString &projectNamespace, const QString &projectName, bool withAuth ) +bool MerginApi::pullProject( const QString &projectNamespace, const QString &projectName, bool withAuth, bool ignoreSelectiveSync ) { QString projectFullName = getFullProjectName( projectNamespace, projectName ); bool pullHasStarted = false; @@ -1308,10 +1303,34 @@ bool MerginApi::pullProject( const QString &projectNamespace, const QString &pro 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; + TransactionStatus transaction; + + transaction.replyPullProjectInfo = reply; + transaction.type = TransactionStatus::Pull; + + LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); + if ( !projectInfo.isValid() ) + { + // first-time download + transaction.firstTimeDownload = true; + transaction.ignoreSelectiveSync = ignoreSelectiveSync; + } + else + { + // regular pull, check if we ignore selective sync from the file flag + bool selectiveSyncIgnored = QFileInfo::exists( projectInfo.projectDir + "/" + sIgnoreSelectiveSyncFileFlag ); + if ( selectiveSyncIgnored ) + { + transaction.ignoreSelectiveSync = true; + } + } + + if ( transaction.ignoreSelectiveSync ) + { + CoreUtils::log( "pull " + projectFullName, "Ignoring selective sync (mergin-config.json)!" ); + } + + mTransactionalStatus.insert( projectFullName, transaction ); emit syncProjectStatusChanged( projectFullName, 0 ); @@ -2301,12 +2320,12 @@ LocalProject MerginApi::getLocalProject( const QString &projectFullName ) return mLocalProjects.projectFromMerginName( projectFullName ); } -ProjectDiff MerginApi::localProjectChanges( const QString &projectDir ) +ProjectDiff MerginApi::localChanges( const QString &projectDir ) { MerginProjectMetadata projectMetadata = MerginProjectMetadata::fromCachedJson( projectDir + "/" + sMetadataFile ); QList localFiles = getLocalProjectFiles( projectDir + "/" ); - MerginConfig config = MerginConfig::fromFile( projectDir + "/" + sMerginConfigFile ); + SelectiveSyncConfig config = SelectiveSyncConfig::fromFile( projectDir + "/" + sSelectiveSyncConfigFile ); return compareProjectFiles( projectMetadata.files, projectMetadata.files, localFiles, projectDir, config.isValid, config ); } @@ -2352,27 +2371,33 @@ bool MerginApi::parseVersion( const QString &version, int &major, int &minor, in return true; } -bool MerginApi::hasLocalProjectChanges( const QString &projectFullName ) +bool MerginApi::hasLocalChanges( const QString &projectDir ) { - const LocalProject localProject = mLocalProjects.projectFromMerginName( projectFullName ); - const QString metadataFilePath = localProject.projectDir + "/" + sMetadataFile; + if ( projectDir.isEmpty() || !QFileInfo::exists( projectDir ) ) + { + return false; // invalid project dir + } + + const QString metadataFilePath = projectDir + "/" + sMetadataFile; // If the project does not have metadata file, there are local changes - if ( !QFile::exists( metadataFilePath ) ) + if ( !QFileInfo::exists( metadataFilePath ) ) { return true; } const MerginProjectMetadata projectMetadata = MerginProjectMetadata::fromCachedJson( metadataFilePath ); - const QList localFiles = getLocalProjectFiles( localProject.projectDir + "/" ); + const QList localFiles = getLocalProjectFiles( projectDir + "/" ); - MerginConfig config; - if ( supportsSelectiveSync() ) + SelectiveSyncConfig config; + bool selectiveSyncIgnored = QFileInfo::exists( projectDir + "/" + sIgnoreSelectiveSyncFileFlag ); + + if ( !selectiveSyncIgnored ) { - config = MerginConfig::fromFile( localProject.projectDir + "/" + sMerginConfigFile ); + config = SelectiveSyncConfig::fromFile( projectDir + "/" + sSelectiveSyncConfigFile ); } - return hasLocalChanges( projectMetadata.files, localFiles, localProject.projectDir, config ); + return hasLocalChanges( projectMetadata.files, localFiles, projectDir, config ); } QString MerginApi::getTempProjectDir( const QString &projectFullName ) @@ -2644,7 +2669,7 @@ bool MerginApi::finalizeProjectPullApplyDiff( const QString &projectFullName, co createPathIfNotExists( src ); createPathIfNotExists( dest ); - createPathIfNotExists( basefile ); + createPathIfNotExists( basefile ); // todo: how could this not exist? QStringList diffFiles; for ( const auto &item : items ) @@ -3108,7 +3133,7 @@ void MerginApi::pushV2FileReplyFinished() } Q_ASSERT( found ); - CoreUtils::log( "push " + projectFullName, QStringLiteral( "Uploading file: %1, chunk %2/%3 successful, received id: %4, valid until: %5" ).arg( processedFile.path ).arg( chunkNo ).arg( processedFile.chunks.count() ).arg( chunk.id ).arg( chunk.valid_until.toString() ) ); + CoreUtils::log( "push " + projectFullName, QStringLiteral( "Uploading file: %1, chunk %2/%3 successful, received id: %4, valid until: %5" ).arg( processedFile.path ).arg( chunkNo + 1).arg( processedFile.chunks.count() ).arg( chunk.id ).arg( chunk.valid_until.toString() ) ); bool fileFullyUploaded = chunkNo == ( processedFile.chunks.size() - 1 ); @@ -3296,16 +3321,17 @@ void MerginApi::prepareProjectPull( const QString &projectFullName, const QByteA QString downloadInProgressFilePath = CoreUtils::downloadInProgressFilePath( transaction.projectDir ); createPathIfNotExists( downloadInProgressFilePath ); if ( !CoreUtils::createEmptyFile( downloadInProgressFilePath ) ) + { CoreUtils::log( QStringLiteral( "pull %1" ).arg( projectFullName ), "Unable to create temporary download in progress file" ); + } CoreUtils::log( "pull " + projectFullName, QStringLiteral( "First time download - new directory: " ) + transaction.projectDir ); } Q_ASSERT( !transaction.projectDir.isEmpty() ); // that would mean we do not have entry -> fail getting local files - if ( transaction.configAllowed ) + if ( !transaction.ignoreSelectiveSync ) { - prepareDownloadConfig( projectFullName ); } else { @@ -3321,7 +3347,7 @@ void MerginApi::startProjectPull( const QString &projectFullName ) 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 ); + SelectiveSyncConfig oldTransactionConfig = SelectiveSyncConfig::fromFile( transaction.projectDir + "/" + sSelectiveSyncConfigFile ); CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Updating from version %1 to version %2" ) .arg( oldServerProject.version ).arg( serverProject.version ) ); @@ -3331,8 +3357,8 @@ void MerginApi::startProjectPull( const QString &projectFullName ) serverProject.files, localFiles, transaction.projectDir, - transaction.configAllowed, - transaction.config, + !transaction.ignoreSelectiveSync, + transaction.selectiveSync, oldTransactionConfig ); CoreUtils::log( "pull " + projectFullName, transaction.diff.dump() ); @@ -3444,7 +3470,7 @@ void MerginApi::startProjectPull( const QString &projectFullName ) } } -void MerginApi::prepareDownloadConfig( const QString &projectFullName, bool downloaded ) +void MerginApi::prepareSelectiveSyncConfig( const QString &projectFullName, bool downloaded ) { Q_ASSERT( mTransactionalStatus.contains( projectFullName ) ); TransactionStatus &transaction = mTransactionalStatus[projectFullName]; @@ -3453,7 +3479,7 @@ void MerginApi::prepareDownloadConfig( const QString &projectFullName, bool down const auto res = std::find_if( newServerVersion.files.begin(), newServerVersion.files.end(), []( const MerginFile & file ) { - return file.path == sMerginConfigFile; + return file.path == sSelectiveSyncConfigFile; } ); bool serverContainsConfig = res != newServerVersion.files.end(); @@ -3462,7 +3488,7 @@ void MerginApi::prepareDownloadConfig( const QString &projectFullName, bool down if ( !downloaded ) { // we should have server config but we do not have it yet - return requestServerConfig( projectFullName ); + return requestSelectiveSyncConfig( projectFullName ); } } @@ -3470,22 +3496,22 @@ void MerginApi::prepareDownloadConfig( const QString &projectFullName, bool down const auto resOld = std::find_if( oldServerVersion.files.begin(), oldServerVersion.files.end(), []( const MerginFile & file ) { - return file.path == sMerginConfigFile; + return file.path == sSelectiveSyncConfigFile; } ); bool previousVersionContainedConfig = ( resOld != oldServerVersion.files.end() ) && !transaction.firstTimeDownload; - if ( !transaction.config.isValid ) + if ( !transaction.selectiveSync.isValid ) { - // if transaction is not valid (or missing), consider it as deleted - transaction.config.downloadMissingFiles = true; - CoreUtils::log( "MerginConfig", "No config detected" ); + // if transaction's config is not valid (or missing), consider it as deleted + transaction.selectiveSync.downloadMissingFiles = true; + CoreUtils::log( "Selective sync", "No config detected" ); } else if ( serverContainsConfig && previousVersionContainedConfig ) { // config was there, check if there are changes - QString newChk = newServerVersion.fileInfo( sMerginConfigFile ).checksum; - QString oldChk = oldServerVersion.fileInfo( sMerginConfigFile ).checksum; + QString newChk = newServerVersion.fileInfo( sSelectiveSyncConfigFile ).checksum; + QString oldChk = oldServerVersion.fileInfo( sSelectiveSyncConfigFile ).checksum; if ( newChk == oldChk ) { @@ -3494,54 +3520,51 @@ void MerginApi::prepareDownloadConfig( const QString &projectFullName, bool down else { // config was changed, but what changed? - MerginConfig oldConfig = MerginConfig::fromFile( transaction.projectDir + "/" + MerginApi::sMerginConfigFile ); + SelectiveSyncConfig oldConfig = SelectiveSyncConfig::fromFile( transaction.projectDir + "/" + MerginApi::sSelectiveSyncConfigFile ); - if ( oldConfig.selectiveSyncEnabled != transaction.config.selectiveSyncEnabled ) + if ( oldConfig.enabled != transaction.selectiveSync.enabled ) { // selective sync was enabled/disabled - if ( transaction.config.selectiveSyncEnabled ) + if ( transaction.selectiveSync.enabled ) { - CoreUtils::log( "MerginConfig", "Selective sync has been enabled" ); + CoreUtils::log( "Selective sync", "Selective sync has been enabled" ); } else { - CoreUtils::log( "MerginConfig", "Selective sync has been disabled, downloading missing files." ); - transaction.config.downloadMissingFiles = true; + CoreUtils::log( "Selective sync", "Selective sync has been disabled, downloading missing files." ); + transaction.selectiveSync.downloadMissingFiles = true; } } - else if ( oldConfig.selectiveSyncDir != transaction.config.selectiveSyncDir ) + else if ( oldConfig.dir != transaction.selectiveSync.dir ) { - CoreUtils::log( "MerginConfig", "Selective sync directory has changed, downloading missing files." ); - transaction.config.downloadMissingFiles = true; + CoreUtils::log( "Selective sync", "Selective sync directory has changed, downloading missing files." ); + transaction.selectiveSync.downloadMissingFiles = true; } else { - CoreUtils::log( "MerginConfig", "Unknown change in config file, continuing with latest version." ); + CoreUtils::log( "Selective sync", "Unknown change in config file, continuing with latest version." ); } } } - else if ( serverContainsConfig ) + else if ( serverContainsConfig ) // and previous project version did not { - CoreUtils::log( "MerginConfig", "Detected new config file." ); + CoreUtils::log( "Selective sync", "Detected new config file." ); } - else if ( previousVersionContainedConfig ) // and current does not + else if ( previousVersionContainedConfig ) // and current project version does not { - CoreUtils::log( "MerginConfig", "Config file was removed, downloading missing files." ); - transaction.config.downloadMissingFiles = true; + CoreUtils::log( "Selective sync", "Config file was removed, downloading missing files." ); + transaction.selectiveSync.downloadMissingFiles = true; } else // no config in last versions { // pull like without config - transaction.configAllowed = false; - transaction.config.isValid = false; - - // if it would be possible to add mergin-config locally, it needs to be checked here + transaction.selectiveSync.enabled = false; } startProjectPull( projectFullName ); } -void MerginApi::requestServerConfig( const QString &projectFullName ) +void MerginApi::requestSelectiveSyncConfig( const QString &projectFullName ) { Q_ASSERT( mTransactionalStatus.contains( projectFullName ) ); TransactionStatus &transaction = mTransactionalStatus[projectFullName]; @@ -3549,7 +3572,7 @@ void MerginApi::requestServerConfig( const QString &projectFullName ) QUrl url( mApiRoot + QStringLiteral( "/v1/project/raw/" ) + projectFullName ); QUrlQuery query; - query.addQueryItem( "file", sMerginConfigFile.toUtf8().toPercentEncoding() ); + query.addQueryItem( "file", sSelectiveSyncConfigFile.toUtf8().toPercentEncoding() ); query.addQueryItem( "version", QStringLiteral( "v%1" ).arg( transaction.version ) ); url.setQuery( query ); @@ -3559,9 +3582,9 @@ void MerginApi::requestServerConfig( const QString &projectFullName ) Q_ASSERT( !transaction.replyPullServerConfig ); transaction.replyPullServerConfig = mManager->get( request ); - connect( transaction.replyPullServerConfig, &QNetworkReply::finished, this, &MerginApi::cacheServerConfig ); + connect( transaction.replyPullServerConfig, &QNetworkReply::finished, this, &MerginApi::cacheSelectiveSyncConfig ); - CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Requesting mergin config: " ) + url.toString() ); + CoreUtils::log( "pull " + projectFullName, QStringLiteral( "Requesting selective sync config: " ) + url.toString() ); } QList MerginApi::itemsForFileChunks( const MerginFile &file, int version ) @@ -3806,12 +3829,12 @@ bool MerginApi::hasLocalChanges( const QList &oldServerFiles, const QList &localFiles, const QString &projectDir, - const MerginConfig config + const SelectiveSyncConfig config ) { QList resolvedOldServerFiles; - if ( config.isValid ) // if a config was set, selective sync is supported + if ( config.isValid ) // if a config was set, selective sync is supported -- not true, we should just always use the config if it is there { for ( const MerginFile &file : oldServerFiles ) { @@ -3824,7 +3847,7 @@ bool MerginApi::hasLocalChanges( resolvedOldServerFiles = oldServerFiles; } - if ( localFiles.count() != resolvedOldServerFiles.count() ) + if ( localFiles.count() != resolvedOldServerFiles.count() ) //TODO: this is most likely problematic when selective sync is enabled, but the mobile app has some images cached from before! { return true; } @@ -3885,8 +3908,8 @@ ProjectDiff MerginApi::compareProjectFiles( const QList &localFiles, const QString &projectDir, bool allowConfig, - const MerginConfig &config, - const MerginConfig &lastSyncConfig + const SelectiveSyncConfig &config, + const SelectiveSyncConfig &lastSyncConfig ) { ProjectDiff diff; @@ -4245,6 +4268,16 @@ void MerginApi::finishTransaction( const QString &projectFullName, bool syncSucc // update the local metadata file writeData( transaction.projectMetadata, transaction.projectDir + "/" + MerginApi::sMetadataFile ); + if ( transaction.firstTimeDownload && transaction.ignoreSelectiveSync ) + { + // store info that this project is ignoring selective sync + if ( !CoreUtils::createEmptyFile( transaction.projectDir + "/" + sIgnoreSelectiveSyncFileFlag ) ) + { + CoreUtils::log( "sync " + projectFullName, QStringLiteral( "Failed to create %1" ).arg( sIgnoreSelectiveSyncFileFlag ) ); + // not a big deal though, used only in tests + } + } + // update info of local projects mLocalProjects.updateLocalVersion( transaction.projectDir, transaction.version ); @@ -4264,7 +4297,7 @@ void MerginApi::finishTransaction( const QString &projectFullName, bool syncSucc emit projectReloadNeededAfterSync( projectFullName ); } - const bool versionUpToDate = mLocalProjects.projectFromMerginName( projectFullName ).localVersion == transaction.version; + const bool versionUpToDate = mLocalProjects.projectFromMerginName( projectFullName ).localVersion == transaction.version; // Watch out, this is likely bug - always results to TRUE after sync const TransactionStatus::TransactionType transactionType = transaction.type; mTransactionalStatus.remove( projectFullName ); @@ -4327,9 +4360,9 @@ bool MerginApi::isInIgnore( const QFileInfo &info ) return sIgnoreExtensions.contains( info.suffix() ) || sIgnoreFiles.contains( info.fileName() ) || info.filePath().contains( sMetadataFolder + "/" ); } -bool MerginApi::excludeFromSync( const QString &filePath, const MerginConfig &config ) +bool MerginApi::excludeFromSync( const QString &filePath, const SelectiveSyncConfig &config ) { - if ( config.isValid && config.selectiveSyncEnabled ) + if ( config.isValid && config.enabled ) { QFileInfo info( filePath ); @@ -4338,11 +4371,11 @@ bool MerginApi::excludeFromSync( const QString &filePath, const MerginConfig &co if ( !isExcludedFormat ) return false; - if ( config.selectiveSyncDir.isEmpty() ) + if ( config.dir.isEmpty() ) { return true; // we are ignoring photos in the entire project } - else if ( filePath.startsWith( config.selectiveSyncDir ) ) + else if ( filePath.startsWith( config.dir ) ) { return true; // we are ignoring photo in subfolder } diff --git a/core/merginapi.h b/core/merginapi.h index f4a8f8671..56b7047f7 100644 --- a/core/merginapi.h +++ b/core/merginapi.h @@ -195,8 +195,8 @@ struct TransactionStatus ProjectDiff diff; - bool configAllowed = false; //!< if true, seeks for mergin-config and alters synchronization process based on it - MerginConfig config; //!< defines additional behavior of the transaction (e.g. selective sync) + SelectiveSyncConfig selectiveSync; //!< defines if selective sync is used in this project and if so, in which folder. Read from mergin-config.json + bool ignoreSelectiveSync = false; //!< if true, treats mergin-config.json as a regular file, does not use selective sync. Useful for testing. Cached after initial download and automatically reused in further syncs. TransactionType type; }; @@ -229,8 +229,6 @@ class MerginApi: public QObject Q_PROPERTY( MerginSubscriptionInfo *subscriptionInfo READ subscriptionInfo NOTIFY subscriptionInfoChanged ) Q_PROPERTY( QString apiRoot READ apiRoot WRITE setApiRoot NOTIFY apiRootChanged ) Q_PROPERTY( bool apiSupportsSubscriptions READ apiSupportsSubscriptions NOTIFY apiSupportsSubscriptionsChanged ) - // supportsSelectiveSync if true, fetches mergin-config.json in project and changes sync behavior based on its content (selective sync) - Q_PROPERTY( bool supportsSelectiveSync READ supportsSelectiveSync NOTIFY supportsSelectiveSyncChanged ) Q_PROPERTY( /*MerginApiStatus::ApiStatus*/ int apiVersionStatus READ apiVersionStatus NOTIFY apiVersionStatusChanged ) Q_PROPERTY( /*MerginServerType::ServerType*/ int serverType READ serverType NOTIFY serverTypeChanged ) Q_PROPERTY( bool apiSupportsWorkspaces READ apiSupportsWorkspaces NOTIFY apiSupportsWorkspacesChanged ) @@ -294,9 +292,10 @@ class MerginApi: public QObject * \param projectNamespace Project's namespace used in request. * \param projectName Project's name used in request. * \param withAuth If True, request is constructed with current authorization + * \param ignoreSelectiveSync If True, this project is downloaded without support for selective sync < &localFiles, const QString &projectDir, bool allowConfig = false, - const MerginConfig &config = MerginConfig(), - const MerginConfig &lastSyncConfig = MerginConfig() + const SelectiveSyncConfig &config = {}, + const SelectiveSyncConfig &lastSyncConfig = {} ); + static ProjectDiff localChanges( const QString &projectDir ); + /** * Finds if project files from two sources are same * - "old" server version (what was downloaded from server) - read from the project directory's stored metadata @@ -529,14 +528,18 @@ class MerginApi: public QObject * - there is any local file missing in "old" server version files * - there is different checksum of any non-diffable file (e.g. CSV file) * - there is different content of any diffable file (e.g. GeoPackage) + * + * The function returns early, compared to localChanges() that returns the full ProjectDiff */ + static bool hasLocalChanges( const QString &projectDir ); static bool hasLocalChanges( const QList &oldServerFiles, const QList &localFiles, const QString &projectDir, - const MerginConfig config + const SelectiveSyncConfig config ); + static QList getLocalProjectFiles( const QString &projectPath ); QString apiRoot() const; @@ -561,10 +564,10 @@ class MerginApi: public QObject * 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. + * @param config SelectiveSyncConfig parsed from JSON, selective-sync properties are read from it. * @return True, if a file at given filePath suppose to be excluded from sync. */ - static bool excludeFromSync( const QString &filePath, const MerginConfig &config ); + static bool excludeFromSync( const QString &filePath, const SelectiveSyncConfig &config ); bool apiSupportsSubscriptions() const; void setApiSupportsSubscriptions( bool apiSupportsSubscriptions ); @@ -578,9 +581,6 @@ class MerginApi: public QObject */ static bool extractProjectName( const QString &sourceString, QString &projectNamespace, QString &projectName ); // todo: go to utils - bool supportsSelectiveSync() const; - void setSupportsSelectiveSync( bool supportsSelectiveSync ); - /** * Determine Mergin server type by querying /config endpoint. * Possible types are: saas, ce, ee and legacy @@ -695,7 +695,6 @@ class MerginApi: public QObject signals: void apiSupportsSubscriptionsChanged(); - void supportsSelectiveSyncChanged(); void listProjectsFinished( const MerginProjectsList &merginProjects, int projectCount, int page, QString requestId ); void listProjectsFailed(); @@ -740,7 +739,6 @@ class MerginApi: public QObject void workspaceInfoChanged(); void subscriptionInfoChanged(); void activeWorkspaceChanged(); - void configChanged(); void pingMerginFinished( const QString &apiVersion, bool serverSupportsSubscriptions, const QString &msg ); void pullFilesStarted(); void pushFilesStarted(); @@ -794,7 +792,7 @@ class MerginApi: public QObject // Pull slots void pullInfoReplyFinished(); void downloadItemReplyFinished( DownloadQueueItem item ); - void cacheServerConfig(); + void cacheSelectiveSyncConfig(); // Push slots void pushStartReplyFinished(); @@ -913,9 +911,9 @@ class MerginApi: public QObject void startProjectPull( const QString &projectFullName ); - //! 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 ); + //! Takes care of finding the correct selective sync config file, appends it to current transaction and proceeds with project pull + void prepareSelectiveSyncConfig( const QString &projectFullName, bool downloaded = false ); + void requestSelectiveSyncConfig( const QString &projectFullName ); //! Starts download request of another item void downloadNextItem( const QString &projectFullName ); @@ -978,7 +976,6 @@ class MerginApi: public QObject QEventLoop mAuthLoopEvent; MerginApiStatus::VersionStatus mApiVersionStatus = MerginApiStatus::VersionStatus::UNKNOWN; bool mApiSupportsSubscriptions = false; - bool mSupportsSelectiveSync = true; bool mApiSupportsSso = false; bool mUserSelfRegistrationEnabled = false; QString mApiVersion; diff --git a/core/merginprojectmetadata.cpp b/core/merginprojectmetadata.cpp index d77ff555a..a082933d6 100644 --- a/core/merginprojectmetadata.cpp +++ b/core/merginprojectmetadata.cpp @@ -145,40 +145,39 @@ MerginFile MerginProjectMetadata::fileInfo( const QString &filePath ) const if ( merginFile.path == filePath ) return merginFile; } - qDebug() << "requested fileInfo() for non-existant file! " << filePath; + return {}; } -MerginConfig MerginConfig::fromJson( const QByteArray &data ) +SelectiveSyncConfig SelectiveSyncConfig::fromJson( const QByteArray &data ) { QJsonDocument doc = QJsonDocument::fromJson( data ); - MerginConfig config; + SelectiveSyncConfig config; if ( doc.isObject() ) { QJsonObject docObj = doc.object(); - config.selectiveSyncEnabled = docObj.value( QStringLiteral( "input-selective-sync" ) ).toBool( false ); - config.selectiveSyncDir = docObj.value( QStringLiteral( "input-selective-sync-dir" ) ).toString(); + config.enabled = docObj.value( QStringLiteral( "input-selective-sync" ) ).toBool( false ); + config.dir = docObj.value( QStringLiteral( "input-selective-sync-dir" ) ).toString(); config.isValid = true; } else { - CoreUtils::log( QStringLiteral( "MerginConfig" ), QStringLiteral( "Invalid content of a config file!" ) ); + CoreUtils::log( QStringLiteral( "Selective sync" ), QStringLiteral( "Invalid content of mergin-config.json file!" ) ); } return config; } -MerginConfig MerginConfig::fromFile( const QString &filePath ) +SelectiveSyncConfig SelectiveSyncConfig::fromFile( const QString &filePath ) { - MerginConfig config; QFile file( filePath ); if ( file.open( QIODevice::ReadOnly ) ) { QByteArray data = file.readAll(); - config = MerginConfig::fromJson( data ); + return SelectiveSyncConfig::fromJson( data ); } - return config; + return {}; } diff --git a/core/merginprojectmetadata.h b/core/merginprojectmetadata.h index 38a58cc58..a159d71bf 100644 --- a/core/merginprojectmetadata.h +++ b/core/merginprojectmetadata.h @@ -49,16 +49,16 @@ struct MerginFile static MerginFile fromJsonObject( const QJsonObject &merginFileInfo ); }; -//! MerginConfig stored in .mergin-config.json, describing optional sync behavior -struct MerginConfig +//! SelectiveSyncConfig represents parsed mergin-config.json file. It defines if the client should download images from others or not. +struct SelectiveSyncConfig { - bool selectiveSyncEnabled = false; - QString selectiveSyncDir; + bool enabled = false; + QString dir; bool isValid = false; bool downloadMissingFiles = false; //!< indicates that this sync must download all files that are missing (excluding selective dir), because config was removed/changed - static MerginConfig fromJson( const QByteArray &data ); - static MerginConfig fromFile( const QString &projectDir ); + static SelectiveSyncConfig fromJson( const QByteArray &data ); + static SelectiveSyncConfig fromFile( const QString &projectDir ); }; //! Metadata read from project info reply or read from cached local .mergin.json file diff --git a/core/merginprojectstatusmodel.cpp b/core/merginprojectstatusmodel.cpp index ee8861c71..577981c26 100644 --- a/core/merginprojectstatusmodel.cpp +++ b/core/merginprojectstatusmodel.cpp @@ -128,7 +128,7 @@ bool MerginProjectStatusModel::loadProjectInfo( const QString &projectFullName ) LocalProject projectInfo = mLocalProjects.projectFromMerginName( projectFullName ); if ( !projectInfo.projectDir.isEmpty() ) { - ProjectDiff diff = MerginApi::localProjectChanges( projectInfo.projectDir ); + ProjectDiff diff = MerginApi::localChanges( projectInfo.projectDir ); bool hasLocalChanges = !diff.localAdded.isEmpty() || !diff.localUpdated.isEmpty() || !diff.localDeleted.isEmpty(); diff --git a/core/project.cpp b/core/project.cpp index 2ac72260d..5f1b10b12 100644 --- a/core/project.cpp +++ b/core/project.cpp @@ -33,7 +33,7 @@ QString MerginProject::id() const return MerginApi::getFullProjectName( projectNamespace, projectName ); } -ProjectStatus::Status ProjectStatus::projectStatus( const Project &project, MerginApi &merginApi ) +ProjectStatus::Status ProjectStatus::projectStatus( const Project &project ) { if ( !project.isMergin() || !project.isLocal() ) // This is not a Mergin project or not downloaded project return ProjectStatus::NoVersion; @@ -50,7 +50,7 @@ ProjectStatus::Status ProjectStatus::projectStatus( const Project &project, Merg return ProjectStatus::NeedsSync; } - if ( merginApi.hasLocalProjectChanges( project.fullName() ) ) + if ( MerginApi::hasLocalChanges( project.local.projectDir ) ) { return ProjectStatus::NeedsSync; } diff --git a/core/project.h b/core/project.h index 50da12615..e3fb3c6df 100644 --- a/core/project.h +++ b/core/project.h @@ -35,7 +35,7 @@ class ProjectStatus Q_ENUM( Status ) //! Returns project state from ProjectStatus::Status enum for the project - static Status projectStatus( const Project &project, MerginApi &merginApi ); + static Status projectStatus( const Project &project ); }; /** diff --git a/core/synchronizationmanager.cpp b/core/synchronizationmanager.cpp index a3b1755e4..2a4001682 100644 --- a/core/synchronizationmanager.cpp +++ b/core/synchronizationmanager.cpp @@ -204,8 +204,8 @@ void SynchronizationManager::onTransactionFinished( const QString &finishedProje return; } - // a push just finished - go for another pull if there are still local changes to sync - if ( mMerginApi->hasLocalProjectChanges( finishedProjectFullName ) ) + // a push just finished - repeat the sync cycle if there are still local changes to sync + if ( MerginApi::hasLocalChanges( mMerginApi->getLocalProject( finishedProjectFullName ).projectDir ) ) { QString projectNamespace, projectName; MerginApi::extractProjectName( finishedProjectFullName, projectNamespace, projectName ); From 99aebcfd89b89a4d6013721da953deb4b39b6a8c Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Sat, 26 Sep 2026 00:40:03 +0200 Subject: [PATCH 2/5] Fix errors in autotests - 1 --- core/merginapi.cpp | 8 ++++++-- core/merginapi.h | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/core/merginapi.cpp b/core/merginapi.cpp index 09610de72..3b3de751f 100644 --- a/core/merginapi.cpp +++ b/core/merginapi.cpp @@ -4477,7 +4477,7 @@ void MerginApi::updateWorkspaceService( const QString &workspaceId, const QStrin connect( reply, &QNetworkReply::finished, this, &MerginApi::updateWorkspaceStorageLimitFinished ); CoreUtils::log( QStringLiteral( "update workspace storage limit" ), - QStringLiteral( "Updating workspace: " ).arg( url.toString() ) ); + QStringLiteral( "Updating workspace: %1" ).arg( url.toString() ) ); } void MerginApi::updateWorkspaceStorageLimitFinished() @@ -4488,12 +4488,16 @@ void MerginApi::updateWorkspaceStorageLimitFinished() if ( r->error() == QNetworkReply::NoError ) { CoreUtils::log( QStringLiteral( "update workspace storage limit" ), QStringLiteral( "Successfully updated workspace limits" ) ); + + emit workspaceLimitsUpdated(); } else { CoreUtils::log( QStringLiteral( "update workspace storage limit" ), - QStringLiteral( "Error: " ).arg( r->errorString() ) ); + QStringLiteral( "Error: %1" ).arg( r->errorString() ) ); } + + r->deleteLater(); } void MerginApi::getServerConfig() diff --git a/core/merginapi.h b/core/merginapi.h index 56b7047f7..6957c7654 100644 --- a/core/merginapi.h +++ b/core/merginapi.h @@ -785,6 +785,8 @@ class MerginApi: public QObject void userSelfRegistrationEnabledChanged(); + void workspaceLimitsUpdated(); + private slots: void listProjectsReplyFinished( QString requestId ); void listProjectsByNameReplyFinished( QString requestId ); From 304cbec34f038f550f9ba63e934e32b87c44dcab Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Sat, 26 Sep 2026 00:41:17 +0200 Subject: [PATCH 3/5] Fix errors in autotests - 2 - lookup project id after project creation --- core/merginapi.cpp | 60 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/core/merginapi.cpp b/core/merginapi.cpp index 3b3de751f..d948687fc 100644 --- a/core/merginapi.cpp +++ b/core/merginapi.cpp @@ -248,18 +248,59 @@ bool MerginApi::pushProject( const QString &projectNamespace, const QString &pro transaction.projectDir = projectInfo.projectDir; Q_ASSERT( !transaction.projectDir.isEmpty() ); - if ( projectInfo.localVersion == -1 ) + if ( transaction.isInitialPush ) { - // TODO: this is project creation + // We do not know the project ID because project creation API + // does not send it out ATM, we need to find the project and parse the project ID + + // NOTE: this is a workaround until server is fixed... + QNetworkReply *reply = getProjectInfo( projectFullName, mUserAuth->hasAuthData() ); + + if ( !reply ) + { + return false; + } + + mTransactionalStatus.insert( projectFullName, transaction ); + + CoreUtils::log( QStringLiteral( "Project id lookup" ), QStringLiteral( "Looking for ID of the freshly baked project - %1" ).arg( projectFullName ) ); + + mTransactionalStatus.insert( projectFullName, transaction ); + + connect( reply, &QNetworkReply::finished, this, [this, projectFullName](){ + + QNetworkReply *r = qobject_cast( sender() ); + Q_ASSERT( r ); + + if ( r->error() == QNetworkReply::NoError ) + { + const QByteArray data = r->readAll(); + + const MerginProjectMetadata projectMetadata = MerginProjectMetadata::fromJson( data ); + + mTransactionalStatus[projectFullName].projectId = projectMetadata.id; + mTransactionalStatus[projectFullName].version = 0; + CoreUtils::log( "Project id lookup", QStringLiteral( "Found the project! ID: %1" ).arg( projectMetadata.id ) ); + + preparePushPayload( projectFullName ); + } + else + { + CoreUtils::log( "Project id lookup", QStringLiteral( "Error occured, finito." ) ); + } + + r->deleteLater(); + return; + }); } else { transaction.version = projectInfo.localVersion; - } - mTransactionalStatus.insert( projectFullName, transaction ); + mTransactionalStatus.insert( projectFullName, transaction ); - preparePushPayload( projectFullName ); + preparePushPayload( projectFullName ); + } } return true; @@ -328,8 +369,14 @@ void MerginApi::preparePushPayload( const QString &projectFullName ) QList localFiles = getLocalProjectFiles( transaction.projectDir + "/" ); MerginProjectMetadata oldServerProject = MerginProjectMetadata::fromCachedJson( transaction.projectDir + "/" + sMetadataFile ); - transaction.projectId = oldServerProject.id; + + if ( transaction.projectId.isEmpty() && !oldServerProject.id.isEmpty() ) + { + transaction.projectId = oldServerProject.id; + } + CoreUtils::log( "push " + projectFullName, "Project ID: " + transaction.projectId ); + Q_ASSERT( !transaction.projectId.isEmpty() ); // Check if selective sync is ignored or not in this project bool selectiveSyncIgnored = QFileInfo::exists( transaction.projectDir + "/" + sIgnoreSelectiveSyncFileFlag ); @@ -3332,6 +3379,7 @@ void MerginApi::prepareProjectPull( const QString &projectFullName, const QByteA if ( !transaction.ignoreSelectiveSync ) { + prepareSelectiveSyncConfig( projectFullName ); } else { From 10a4554aaf976823450de137ca0252b6231118e6 Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Sat, 26 Sep 2026 00:41:33 +0200 Subject: [PATCH 4/5] Fix errors in autotests - 3 --- app/test/testutils.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/test/testutils.cpp b/app/test/testutils.cpp index e5dcf87d8..b8aad6bd8 100644 --- a/app/test/testutils.cpp +++ b/app/test/testutils.cpp @@ -182,7 +182,7 @@ void TestUtils::generateRandomUser( MerginApi *api, QString &username, QString & // change the data plan QString workspaceId = QString::number( api->userInfo()->activeWorkspaceId() ); - QSignalSpy wsStorageSpy( api, &MerginApi::updateWorkspaceService ); + QSignalSpy wsStorageSpy( api, &MerginApi::workspaceLimitsUpdated ); // Create JSON payload to change the data plan QString payload = QString( R"({ @@ -200,7 +200,7 @@ void TestUtils::generateRandomUser( MerginApi *api, QString &username, QString & qDebug() << "Updated the storage limit" << username; } -// this needs to be cleared, as the user will be authorized in the test cases. + // this needs to be cleared, as the user will be authorized in the test cases. api->clearAuth(); } From 95e026b0086e7669f13178af6580f688025f5ec1 Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Sat, 26 Sep 2026 00:41:58 +0200 Subject: [PATCH 5/5] Fix errors in autotests - 4 --- app/test/testmerginapi.cpp | 220 +++++++++++++++++++------------------ app/test/testmerginapi.h | 6 +- 2 files changed, 119 insertions(+), 107 deletions(-) diff --git a/app/test/testmerginapi.cpp b/app/test/testmerginapi.cpp index 4195eaa40..255336a0b 100644 --- a/app/test/testmerginapi.cpp +++ b/app/test/testmerginapi.cpp @@ -184,7 +184,7 @@ void TestMerginApi::testDownloadProject() QCOMPARE( mApi->transactions().count(), 0 ); // try to download the project - QSignalSpy spy( mApi, &MerginApi::syncProjectFinished ); + QSignalSpy spy( mApi, &MerginApi::syncTransactionFinished ); mApi->pullProject( projectNamespace, projectName ); QCOMPARE( mApi->transactions().count(), 1 ); QVERIFY( spy.wait( TestUtils::LONG_REPLY * 5 ) ); @@ -250,7 +250,7 @@ void TestMerginApi::testDownloadProjectSpecChars() QVERIFY( projectFile.rename( projectDir + "/" + newProjectFileName ) ); // Upload data - QSignalSpy spy2( mApi, &MerginApi::syncProjectFinished ); + QSignalSpy spy2( mApi, &MerginApi::syncTransactionFinished ); mApi->pushProject( projectNamespace, projectName ); QVERIFY( spy2.wait( TestUtils::LONG_REPLY ) ); QCOMPARE( spy2.count(), 1 ); @@ -275,7 +275,7 @@ void TestMerginApi::testCancelDownloadProject() QString projectDir = mApi->projectsPath() + "/" + projectName + "/"; // Test download and cancel before transaction actually starts - QSignalSpy spy5( mApi, &MerginApi::syncProjectFinished ); + QSignalSpy spy5( mApi, &MerginApi::syncTransactionFinished ); mApi->pullProject( mWorkspaceName, projectName ); QCOMPARE( mApi->transactions().count(), 1 ); mApi->cancelPull( MerginApi::getFullProjectName( mWorkspaceName, projectName ) ); @@ -296,7 +296,7 @@ void TestMerginApi::testCancelDownloadProject() QVERIFY( spy6.wait( TestUtils::LONG_REPLY ) ); QCOMPARE( spy6.count(), 1 ); - QSignalSpy spy7( mApi, &MerginApi::syncProjectFinished ); + QSignalSpy spy7( mApi, &MerginApi::syncTransactionFinished ); mApi->cancelPull( MerginApi::getFullProjectName( mWorkspaceName, projectName ) ); // no need to wait for the signal here - as we call abort() the reply's finished() signal is immediately emitted @@ -434,7 +434,7 @@ void TestMerginApi::testUploadProject() // (this verifies we can cancel upload before a transaction is started) // - QSignalSpy spy( mApi, &MerginApi::syncProjectFinished ); + QSignalSpy spy( mApi, &MerginApi::syncTransactionFinished ); mApi->pushProject( projectNamespace, projectName ); mApi->cancelPush( MerginApi::getFullProjectName( projectNamespace, projectName ) ); @@ -453,7 +453,7 @@ void TestMerginApi::testUploadProject() // (so that we test also cancellation of transaction) // - QSignalSpy spyX( mApi, &MerginApi::syncProjectFinished ); + QSignalSpy spyX( mApi, &MerginApi::syncTransactionFinished ); QSignalSpy spyY( mApi, &MerginApi::pushFilesStarted ); mApi->pushProject( projectNamespace, projectName ); QVERIFY( spyY.wait( TestUtils::LONG_REPLY ) ); @@ -486,7 +486,7 @@ void TestMerginApi::testUploadProject() // mApi->pushProject( projectNamespace, projectName ); - QSignalSpy spy2( mApi, &MerginApi::syncProjectFinished ); + QSignalSpy spy2( mApi, &MerginApi::syncTransactionFinished ); QVERIFY( spy2.wait( TestUtils::LONG_REPLY ) ); QCOMPARE( spy2.count(), 1 ); @@ -767,8 +767,8 @@ void TestMerginApi::testPushNoChanges() QCOMPARE( project2.mergin.serverVersion, 1 ); QCOMPARE( project2.mergin.status, ProjectStatus::UpToDate ); - QCOMPARE( MerginApi::localProjectChanges( projectDir ), ProjectDiff() ); - QVERIFY( !MerginApi::hasLocalProjectChanges( projectDir, mApi->supportsSelectiveSync() ) ); + QCOMPARE( MerginApi::localChanges( projectDir ), ProjectDiff() ); + QVERIFY( !MerginApi::hasLocalChanges( projectDir ) ); } void TestMerginApi::testUpdateAddedFile() @@ -1106,8 +1106,8 @@ void TestMerginApi::testDiffUpload() QVERIFY( QFileInfo::exists( projectDir + "/.mergin/base.gpkg" ) ); - QCOMPARE( MerginApi::localProjectChanges( projectDir ), ProjectDiff() ); // no local changes expected - QVERIFY( !MerginApi::hasLocalProjectChanges( projectDir, mApi->supportsSelectiveSync() ) ); + QCOMPARE( MerginApi::localChanges( projectDir ), ProjectDiff() ); // no local changes expected + QVERIFY( !MerginApi::hasLocalChanges( projectDir ) ); // replace gpkg with a new version with a modified geometry // but make sure time it gets a different timestamp or its checksum will be read from the cache @@ -1115,11 +1115,11 @@ 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 ); + ProjectDiff diff = MerginApi::localChanges( projectDir ); ProjectDiff expectedDiff; expectedDiff.localUpdated = QSet() << "base.gpkg"; QVERIFY2( diff == expectedDiff, diff.dump().toStdString().c_str() ); - QVERIFY( MerginApi::hasLocalProjectChanges( projectDir, mApi->supportsSelectiveSync() ) ); + QVERIFY( MerginApi::hasLocalChanges( projectDir ) ); GeodiffUtils::ChangesetSummary expectedSummary; expectedSummary["simple"] = GeodiffUtils::TableSummary( 0, 1, 0 ); @@ -1129,8 +1129,8 @@ void TestMerginApi::testDiffUpload() uploadRemoteProject( mApi, mWorkspaceName, projectName ); - QCOMPARE( MerginApi::localProjectChanges( projectDir ), ProjectDiff() ); // no local changes expected - QVERIFY( !MerginApi::hasLocalProjectChanges( projectDir, mApi->supportsSelectiveSync() ) ); + QCOMPARE( MerginApi::localChanges( projectDir ), ProjectDiff() ); // no local changes expected + QVERIFY( !MerginApi::hasLocalChanges( projectDir ) ); } void TestMerginApi::testDiffSubdirsUpload() @@ -1145,8 +1145,8 @@ void TestMerginApi::testDiffSubdirsUpload() const QString base( "subdir/subsubdir/base.gpkg" ); QVERIFY( QFileInfo::exists( projectDir + "/.mergin/" + base ) ); - QCOMPARE( MerginApi::localProjectChanges( projectDir ), ProjectDiff() ); // no local changes expected - QVERIFY( !MerginApi::hasLocalProjectChanges( projectDir, mApi->supportsSelectiveSync() ) ); + QCOMPARE( MerginApi::localChanges( projectDir ), ProjectDiff() ); // no local changes expected + QVERIFY( !MerginApi::hasLocalChanges( projectDir ) ); // replace gpkg with a new version with a modified geometry // but make sure time it gets a different timestamp or its checksum will be read from the cache @@ -1154,11 +1154,11 @@ void TestMerginApi::testDiffSubdirsUpload() QVERIFY( QFile::remove( projectDir + "/" + base ) ); QVERIFY( QFile::copy( mTestDataPath + "/modified_1_geom.gpkg", projectDir + "/" + base ) ); - ProjectDiff diff = MerginApi::localProjectChanges( projectDir ); + ProjectDiff diff = MerginApi::localChanges( projectDir ); ProjectDiff expectedDiff; expectedDiff.localUpdated = QSet() << base ; QVERIFY2( diff == expectedDiff, diff.dump().toStdString().c_str() ); - QVERIFY( MerginApi::hasLocalProjectChanges( projectDir, mApi->supportsSelectiveSync() ) ); + QVERIFY( MerginApi::hasLocalChanges( projectDir ) ); GeodiffUtils::ChangesetSummary expectedSummary; expectedSummary["simple"] = GeodiffUtils::TableSummary( 0, 1, 0 ); @@ -1168,8 +1168,8 @@ void TestMerginApi::testDiffSubdirsUpload() uploadRemoteProject( mApi, mWorkspaceName, projectName ); - QCOMPARE( MerginApi::localProjectChanges( projectDir ), ProjectDiff() ); // no local changes expected - QVERIFY( !MerginApi::hasLocalProjectChanges( projectDir, mApi->supportsSelectiveSync() ) ); + QCOMPARE( MerginApi::localChanges( projectDir ), ProjectDiff() ); // no local changes expected + QVERIFY( !MerginApi::hasLocalChanges( projectDir ) ); } void TestMerginApi::testDiffUpdateBasic() @@ -1186,8 +1186,8 @@ void TestMerginApi::testDiffUpdateBasic() downloadRemoteProject( mApi, mWorkspaceName, projectName ); QVERIFY( QFileInfo::exists( projectDir + "/.mergin/base.gpkg" ) ); - QCOMPARE( MerginApi::localProjectChanges( projectDir ), ProjectDiff() ); // no local changes expected - QVERIFY( !MerginApi::hasLocalProjectChanges( projectDir, mApi->supportsSelectiveSync() ) ); + QCOMPARE( MerginApi::localChanges( projectDir ), ProjectDiff() ); // no local changes expected + QVERIFY( !MerginApi::hasLocalChanges( projectDir ) ); QgsVectorLayer *vl0 = new QgsVectorLayer( projectDir + "/base.gpkg|layername=simple", "base", "ogr" ); QVERIFY( vl0->isValid() ); @@ -1216,8 +1216,8 @@ void TestMerginApi::testDiffUpdateBasic() QCOMPARE( vl->featureCount(), static_cast( 4 ) ); delete vl; - QCOMPARE( MerginApi::localProjectChanges( projectDir ), ProjectDiff() ); // no local changes expected - QVERIFY( !MerginApi::hasLocalProjectChanges( projectDir, mApi->supportsSelectiveSync() ) ); + QCOMPARE( MerginApi::localChanges( projectDir ), ProjectDiff() ); // no local changes expected + QVERIFY( !MerginApi::hasLocalChanges( projectDir ) ); QVERIFY( !GeodiffUtils::hasPendingChanges( projectDir, "base.gpkg" ) ); } @@ -1236,8 +1236,8 @@ void TestMerginApi::testDiffUpdateWithRebase() downloadRemoteProject( mApi, mWorkspaceName, projectName ); QVERIFY( QFileInfo::exists( projectDir + "/.mergin/base.gpkg" ) ); - QCOMPARE( MerginApi::localProjectChanges( projectDir ), ProjectDiff() ); // no local changes expected - QVERIFY( !MerginApi::hasLocalProjectChanges( projectDir, mApi->supportsSelectiveSync() ) ); + QCOMPARE( MerginApi::localChanges( projectDir ), ProjectDiff() ); // no local changes expected + QVERIFY( !MerginApi::hasLocalChanges( projectDir ) ); // // download with mApiExtra + modify + upload @@ -1267,11 +1267,11 @@ void TestMerginApi::testDiffUpdateWithRebase() delete vl0; // check that the file is marked as changed - ProjectDiff diff = MerginApi::localProjectChanges( projectDir ); + ProjectDiff diff = MerginApi::localChanges( projectDir ); ProjectDiff expectedDiff; expectedDiff.localUpdated = QSet() << "base.gpkg"; QCOMPARE( diff, expectedDiff ); - QVERIFY( MerginApi::hasLocalProjectChanges( projectDir, mApi->supportsSelectiveSync() ) ); + QVERIFY( MerginApi::hasLocalChanges( projectDir ) ); // check that geodiff knows there was one added feature GeodiffUtils::ChangesetSummary expectedSummary; @@ -1293,9 +1293,9 @@ void TestMerginApi::testDiffUpdateWithRebase() delete vl; // like before the update - there should be locally modified base.gpkg with the changes we did - QCOMPARE( MerginApi::localProjectChanges( projectDir ), expectedDiff ); + QCOMPARE( MerginApi::localChanges( projectDir ), expectedDiff ); QCOMPARE( GeodiffUtils::parseChangesetSummary( changes ), expectedSummary ); - QVERIFY( MerginApi::hasLocalProjectChanges( projectDir, mApi->supportsSelectiveSync() ) ); + QVERIFY( MerginApi::hasLocalChanges( projectDir ) ); } void TestMerginApi::testDiffUpdateWithRebaseFailed() @@ -1315,8 +1315,8 @@ void TestMerginApi::testDiffUpdateWithRebaseFailed() downloadRemoteProject( mApi, mWorkspaceName, projectName ); QVERIFY( QFileInfo::exists( projectDir + "/.mergin/base.gpkg" ) ); - QCOMPARE( MerginApi::localProjectChanges( projectDir ), ProjectDiff() ); // no local changes expected - QVERIFY( !MerginApi::hasLocalProjectChanges( projectDir, mApi->supportsSelectiveSync() ) ); + QCOMPARE( MerginApi::localChanges( projectDir ), ProjectDiff() ); // no local changes expected + QVERIFY( !MerginApi::hasLocalChanges( projectDir ) ); // // download with mApiExtra + modify + upload @@ -1337,12 +1337,12 @@ void TestMerginApi::testDiffUpdateWithRebaseFailed() QVERIFY( r2 && r3 ); // check that the file is marked as changed - ProjectDiff diff = MerginApi::localProjectChanges( projectDir ); + ProjectDiff diff = MerginApi::localChanges( projectDir ); ProjectDiff expectedDiff; expectedDiff.localUpdated = QSet() << "base.gpkg"; qDebug() << diff.dump(); QCOMPARE( diff, expectedDiff ); - QVERIFY( MerginApi::hasLocalProjectChanges( projectDir, mApi->supportsSelectiveSync() ) ); + QVERIFY( MerginApi::hasLocalChanges( projectDir ) ); // check that geodiff knows there was one added feature QString changes = GeodiffUtils::diffableFilePendingChanges( projectDir, "base.gpkg", true ); @@ -1367,8 +1367,8 @@ void TestMerginApi::testDiffUpdateWithRebaseFailed() // there should be a new "conflict" file ProjectDiff expectedDiffFinal; expectedDiffFinal.localAdded = QSet() << conflictFilename; - QCOMPARE( MerginApi::localProjectChanges( projectDir ), expectedDiffFinal ); - QVERIFY( MerginApi::hasLocalProjectChanges( projectDir, mApi->supportsSelectiveSync() ) ); + QCOMPARE( MerginApi::localChanges( projectDir ), expectedDiffFinal ); + QVERIFY( MerginApi::hasLocalChanges( projectDir ) ); } void TestMerginApi::testUpdateWithDiffs() @@ -1385,8 +1385,8 @@ void TestMerginApi::testUpdateWithDiffs() downloadRemoteProject( mApi, mWorkspaceName, projectName ); QVERIFY( QFileInfo::exists( projectDir + "/.mergin/base.gpkg" ) ); - QCOMPARE( MerginApi::localProjectChanges( projectDir ), ProjectDiff() ); // no local changes expected - QVERIFY( !MerginApi::hasLocalProjectChanges( projectDir, mApi->supportsSelectiveSync() ) ); + QCOMPARE( MerginApi::localChanges( projectDir ), ProjectDiff() ); // no local changes expected + QVERIFY( !MerginApi::hasLocalChanges( projectDir ) ); // // download with mApiExtra + modify + upload @@ -1416,8 +1416,8 @@ void TestMerginApi::testUpdateWithDiffs() QCOMPARE( vl->featureCount(), static_cast( 5 ) ); delete vl; - QCOMPARE( MerginApi::localProjectChanges( projectDir ), ProjectDiff() ); - QVERIFY( !MerginApi::hasLocalProjectChanges( projectDir, mApi->supportsSelectiveSync() ) ); + QCOMPARE( MerginApi::localChanges( projectDir ), ProjectDiff() ); + QVERIFY( !MerginApi::hasLocalChanges( projectDir ) ); QVERIFY( !GeodiffUtils::hasPendingChanges( projectDir, "base.gpkg" ) ); } @@ -1477,7 +1477,7 @@ void TestMerginApi::testMigrateProject() // migrate project QSignalSpy spy( mApi, &MerginApi::projectCreated ); - QSignalSpy spy2( mApi, &MerginApi::syncProjectFinished ); + QSignalSpy spy2( mApi, &MerginApi::syncTransactionFinished ); mApi->migrateProjectToMergin( projectName, mWorkspaceName ); @@ -1524,7 +1524,7 @@ void TestMerginApi::testMigrateProjectAndSync() mApi->mLocalProjects.reloadDataDir(); // step 2 QSignalSpy spy( mApi, &MerginApi::projectCreated ); - QSignalSpy spy2( mApi, &MerginApi::syncProjectFinished ); + QSignalSpy spy2( mApi, &MerginApi::syncTransactionFinished ); mApi->migrateProjectToMergin( projectName, mWorkspaceName ); @@ -1582,7 +1582,7 @@ void TestMerginApi::testMigrateDetachProject() // migrate project QSignalSpy spy( mApi, &MerginApi::projectCreated ); - QSignalSpy spy2( mApi, &MerginApi::syncProjectFinished ); + QSignalSpy spy2( mApi, &MerginApi::syncTransactionFinished ); mApi->migrateProjectToMergin( projectName, mWorkspaceName ); @@ -1616,6 +1616,10 @@ void TestMerginApi::testSelectiveSync() createRemoteProject( mApiExtra, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); downloadRemoteProject( mApi, mWorkspaceName, projectName ); + // + // TODO: Clients can not update the config themselves! It must be done over via third client that ignores the selective sync + // + // Create photo files QDir dir; QString photoPath( projectDir + "/subdir" ); @@ -1837,7 +1841,7 @@ void TestMerginApi::testSelectiveSyncRemoveConfig() LocalProjectsManager *serverMirrorProjects = new LocalProjectsManager( serverMirrorDataPath + "/" ); MerginApi *serverMirror = new MerginApi( *serverMirrorProjects, this ); - serverMirror->setSupportsSelectiveSync( false ); + bool ignoreSelectiveSyncForThisClient = true; // Create a project with photos and mergin-config QString projectName = "testSelectiveSyncRemoveConfig"; @@ -1864,7 +1868,9 @@ void TestMerginApi::testSelectiveSyncRemoveConfig() file1.close(); uploadRemoteProject( mApi, mWorkspaceName, projectName ); - downloadRemoteProject( serverMirror, mWorkspaceName, projectName ); + downloadRemoteProject( serverMirror, mWorkspaceName, projectName, ignoreSelectiveSyncForThisClient ); + + QVERIFY( QFileInfo::exists( projectServer + "/" + MerginApi::sIgnoreSelectiveSyncFileFlag ) ); QString configFilePath = projectServer + "/" + "mergin-config.json"; QVERIFY( createJsonFile( configFilePath, @@ -1897,7 +1903,7 @@ void TestMerginApi::testSelectiveSyncRemoveConfig() downloadRemoteProject( mApi, mWorkspaceName, projectName ); downloadRemoteProject( serverMirror, mWorkspaceName, projectName ); - // Let's remove mergin config + // Let's remove the config InputUtils::removeFile( configFilePath ); QVERIFY( !InputUtils::fileExists( configFilePath ) ); @@ -1948,7 +1954,7 @@ void TestMerginApi::testSelectiveSyncDisabledInConfig() LocalProjectsManager *serverMirrorProjects = new LocalProjectsManager( serverMirrorDataPath + "/" ); MerginApi *serverMirror = new MerginApi( *serverMirrorProjects, this ); - serverMirror->setSupportsSelectiveSync( false ); + bool ignoreSelectiveSyncForThisClient = true; // Create a project with photos and mergin-config QString projectName = "testSelectiveSyncDisabledInConfig"; @@ -1960,6 +1966,8 @@ void TestMerginApi::testSelectiveSyncDisabledInConfig() createRemoteProject( mApi, mWorkspaceName, projectName, mTestDataPath + "/" + TEST_PROJECT_NAME + "/" ); downloadRemoteProject( mApi, mWorkspaceName, projectName ); + QVERIFY( !QFileInfo::exists( projectClient1 + "/" + MerginApi::sIgnoreSelectiveSyncFileFlag ) ); + // Create photo files QDir dir; QString photoPathClient1( projectClient1 + "/" + "photos" ); @@ -1975,7 +1983,9 @@ void TestMerginApi::testSelectiveSyncDisabledInConfig() file1.close(); uploadRemoteProject( mApi, mWorkspaceName, projectName ); - downloadRemoteProject( serverMirror, mWorkspaceName, projectName ); + downloadRemoteProject( serverMirror, mWorkspaceName, projectName, ignoreSelectiveSyncForThisClient ); + + QVERIFY( QFileInfo::exists( projectServer + "/" + MerginApi::sIgnoreSelectiveSyncFileFlag ) ); QString configFilePath = projectServer + "/" + "mergin-config.json"; QVERIFY( createJsonFile( configFilePath, @@ -2016,6 +2026,8 @@ void TestMerginApi::testSelectiveSyncDisabledInConfig() downloadRemoteProject( serverMirror, mWorkspaceName, projectName ); + QVERIFY( QFileInfo::exists( projectServer + "/" + MerginApi::sIgnoreSelectiveSyncFileFlag ) ); + // Let's disable selective sync InputUtils::removeFile( configFilePath ); QVERIFY( !InputUtils::fileExists( configFilePath ) ); @@ -2096,7 +2108,7 @@ void TestMerginApi::testSelectiveSyncChangeSyncFolder() LocalProjectsManager *serverMirrorProjects = new LocalProjectsManager( serverMirrorDataPath + "/" ); MerginApi *serverMirror = new MerginApi( *serverMirrorProjects, this ); - serverMirror->setSupportsSelectiveSync( false ); + bool ignoreSelectiveSyncForThisClient = true; // Create a project with photos and mergin-config QString projectName = "testSelectiveSyncChangeSyncFolder"; @@ -2123,7 +2135,9 @@ void TestMerginApi::testSelectiveSyncChangeSyncFolder() file1.close(); uploadRemoteProject( mApi, mWorkspaceName, projectName ); - downloadRemoteProject( serverMirror, mWorkspaceName, projectName ); + downloadRemoteProject( serverMirror, mWorkspaceName, projectName, ignoreSelectiveSyncForThisClient ); + + QVERIFY( QFileInfo::exists( projectServer + "/" + MerginApi::sIgnoreSelectiveSyncFileFlag ) ); QString configFilePath = projectServer + "/" + "mergin-config.json"; QVERIFY( createJsonFile( configFilePath, @@ -2270,7 +2284,7 @@ void TestMerginApi::testSelectiveSyncCorruptedFormat() LocalProjectsManager *serverMirrorProjects = new LocalProjectsManager( serverMirrorDataPath + "/" ); MerginApi *serverMirror = new MerginApi( *serverMirrorProjects, this ); - serverMirror->setSupportsSelectiveSync( false ); + bool ignoreSelectiveSyncForThisClient = true; // Create a project with photos and mergin-config QString projectName = "testSelectiveSyncCorruptedFormat"; @@ -2297,7 +2311,10 @@ void TestMerginApi::testSelectiveSyncCorruptedFormat() file1.close(); uploadRemoteProject( mApi, mWorkspaceName, projectName ); - downloadRemoteProject( serverMirror, mWorkspaceName, projectName ); + downloadRemoteProject( serverMirror, mWorkspaceName, projectName, ignoreSelectiveSyncForThisClient ); + + QVERIFY( QFileInfo::exists( projectServer + "/" + MerginApi::sIgnoreSelectiveSyncFileFlag ) ); + QVERIFY( !QFileInfo::exists( projectClient1 + "/" + MerginApi::sIgnoreSelectiveSyncFileFlag ) ); // add corrupted config file QString configFilePath = projectServer + "/" + "mergin-config.json"; @@ -2566,9 +2583,9 @@ void TestMerginApi::testExcludeFromSync() file.open( QIODevice::WriteOnly ); } - MerginConfig config; - config.selectiveSyncEnabled = true; - config.selectiveSyncDir = selectiveSyncDir; + SelectiveSyncConfig config; + config.enabled = true; + config.dir = selectiveSyncDir; config.isValid = true; QVERIFY( !mApi->excludeFromSync( selectiveSyncDir, config ) ); @@ -2584,11 +2601,11 @@ void TestMerginApi::testExcludeFromSync() QVERIFY( mApi->excludeFromSync( selectiveSyncDir + "/image.JPEG", config ) ); QVERIFY( mApi->excludeFromSync( selectiveSyncDir + "/subdir/image.jpg", config ) ); - config.selectiveSyncDir = selectiveSyncDir + "/subdir"; + config.dir = selectiveSyncDir + "/subdir"; QVERIFY( !mApi->excludeFromSync( selectiveSyncDir + "/image.jpg", config ) ); QVERIFY( mApi->excludeFromSync( selectiveSyncDir + "/subdir/image.jpg", config ) ); - config.selectiveSyncDir.clear(); + config.dir.clear(); QVERIFY( mApi->excludeFromSync( selectiveSyncDir + "/image.jpg", config ) ); } @@ -2626,7 +2643,7 @@ int TestMerginApi::serverVersionFromSpy( QSignalSpy &spy ) { QList response = spy.takeFirst(); - // get version number emited from MerginApi::syncProjectFinished, it is third argument + // get version number emited from MerginApi::syncTransactionFinished, it is third argument if ( response.length() >= 4 ) serverVersion = response.at( 3 ).toInt(); } @@ -2640,6 +2657,14 @@ void TestMerginApi::createRemoteProject( MerginApi *api, const QString &projectN deleteRemoteProjectNow( api, projectNamespace, projectName ); } + // prepare project and the data + QString projectDir = api->projectsPath() + "/" + projectName + "/"; + InputUtils::cpDir( sourcePath, projectDir ); + + + // make LocalProjectsManager aware of the project and its directory + api->localProjectsManager().addLocalProject( projectDir, projectName ); + // create a project QSignalSpy spy( api, &MerginApi::projectCreated ); api->createProject( projectNamespace, projectName, true ); @@ -2647,16 +2672,8 @@ void TestMerginApi::createRemoteProject( MerginApi *api, const QString &projectN QCOMPARE( spy.count(), 1 ); QCOMPARE( spy.takeFirst().at( 1 ).toBool(), true ); - // Copy data - QString projectDir = api->projectsPath() + "/" + projectName + "/"; - InputUtils::cpDir( sourcePath, projectDir ); - - // make MerginApi aware of the project and its directory - api->localProjectsManager().addMerginProject( projectDir, projectNamespace, projectName ); - - // Upload data - QSignalSpy spy3( api, &MerginApi::syncProjectFinished ); - api->pushProject( projectNamespace, projectName ); + // data upload will start automatically once the project is created + QSignalSpy spy3( api, &MerginApi::syncTransactionFinished ); QVERIFY( spy3.wait( TestUtils::LONG_REPLY ) ); QCOMPARE( spy3.count(), 1 ); QList arguments = spy3.takeFirst(); @@ -2748,16 +2765,16 @@ void TestMerginApi::deleteLocalDir( MerginApi *api, const QString &dirPath ) QVERIFY( dir.removeRecursively() ); } -void TestMerginApi::downloadRemoteProject( MerginApi *api, const QString &projectNamespace, const QString &projectName ) +void TestMerginApi::downloadRemoteProject( MerginApi *api, const QString &projectNamespace, const QString &projectName, bool ignoreSelectiveSync ) { int serverVersion; - downloadRemoteProject( api, projectNamespace, projectName, serverVersion ); + downloadRemoteProject( api, projectNamespace, projectName, serverVersion, ignoreSelectiveSync ); } -void TestMerginApi::downloadRemoteProject( MerginApi *api, const QString &projectNamespace, const QString &projectName, int &serverVersion ) +void TestMerginApi::downloadRemoteProject( MerginApi *api, const QString &projectNamespace, const QString &projectName, int &serverVersion, bool ignoreSelectiveSync ) { - QSignalSpy spy( api, &MerginApi::syncProjectFinished ); - api->pullProject( projectNamespace, projectName ); + QSignalSpy spy( api, &MerginApi::syncTransactionFinished ); + api->pullProject( projectNamespace, projectName, true, ignoreSelectiveSync ); QCOMPARE( api->transactions().count(), 1 ); QVERIFY( spy.wait( TestUtils::LONG_REPLY * 5 ) ); serverVersion = serverVersionFromSpy( spy ); @@ -2772,7 +2789,7 @@ void TestMerginApi::uploadRemoteProject( MerginApi *api, const QString &projectN void TestMerginApi::uploadRemoteProject( MerginApi *api, const QString &projectNamespace, const QString &projectName, int &serverVersion ) { api->pushProject( projectNamespace, projectName ); - QSignalSpy spy( api, &MerginApi::syncProjectFinished ); + QSignalSpy spy( api, &MerginApi::syncTransactionFinished ); QVERIFY( spy.wait( TestUtils::LONG_REPLY * 30 ) ); QCOMPARE( spy.count(), 1 ); serverVersion = serverVersionFromSpy( spy ); @@ -3035,7 +3052,7 @@ void TestMerginApi::testDownloadWithNetworkError() // Create signal spies QSignalSpy startSpy( mApi, &MerginApi::pullFilesStarted ); QSignalSpy retrySpy( mApi, &MerginApi::downloadItemRetried ); - QSignalSpy finishSpy( mApi, &MerginApi::syncProjectFinished ); + QSignalSpy finishSpy( mApi, &MerginApi::syncTransactionFinished ); // Trigger the current network error when download starts connect( mApi, &MerginApi::pullFilesStarted, this, [this, failingManager, networkError]() @@ -3093,7 +3110,7 @@ void TestMerginApi::testDownloadWithNetworkErrorRecovery() // Create signal spies QSignalSpy startSpy( mApi, &MerginApi::pullFilesStarted ); QSignalSpy retrySpy( mApi, &MerginApi::downloadItemRetried ); - QSignalSpy finishSpy( mApi, &MerginApi::syncProjectFinished ); + QSignalSpy finishSpy( mApi, &MerginApi::syncTransactionFinished ); // Counter to track retry attempts int retryCount = 0; @@ -3150,10 +3167,10 @@ void TestMerginApi::testDownloadWithNetworkErrorRecovery() delete failingManager; } -void TestMerginApi::testMerginConfigFromFile() +void TestMerginApi::testSelectiveSyncConfigFromFile() { QString tempFilePath; - MerginConfig config; + SelectiveSyncConfig config; // 1 => valid JSON tempFilePath = QDir::tempPath() + "/test_valid_config.json"; @@ -3164,10 +3181,10 @@ void TestMerginApi::testMerginConfigFromFile() file.write( data ); file.close(); } - config = MerginConfig::fromFile( tempFilePath ); + config = SelectiveSyncConfig::fromFile( tempFilePath ); QVERIFY( config.isValid ); - QCOMPARE( config.selectiveSyncEnabled, true ); - QCOMPARE( config.selectiveSyncDir, QString( "photos" ) ); + QCOMPARE( config.enabled, true ); + QCOMPARE( config.dir, QString( "photos" ) ); QFile::remove( tempFilePath ); // 2 => invalid JSON (non-JSON content) @@ -3179,9 +3196,9 @@ void TestMerginApi::testMerginConfigFromFile() file.write( data ); file.close(); } - config = MerginConfig::fromFile( tempFilePath ); + config = SelectiveSyncConfig::fromFile( tempFilePath ); QVERIFY( !config.isValid ); - QCOMPARE( config.selectiveSyncEnabled, false ); + QCOMPARE( config.enabled, false ); QFile::remove( tempFilePath ); // 3 => empty file @@ -3192,16 +3209,16 @@ void TestMerginApi::testMerginConfigFromFile() file.write( "" ); file.close(); } - config = MerginConfig::fromFile( tempFilePath ); + config = SelectiveSyncConfig::fromFile( tempFilePath ); QVERIFY( !config.isValid ); - QCOMPARE( config.selectiveSyncEnabled, false ); + QCOMPARE( config.enabled, false ); QFile::remove( tempFilePath ); // 4 => file-not-found tempFilePath = QDir::tempPath() + "/nonexistent_config.json"; - config = MerginConfig::fromFile( tempFilePath ); + config = SelectiveSyncConfig::fromFile( tempFilePath ); QVERIFY( !config.isValid ); - QCOMPARE( config.selectiveSyncEnabled, false ); + QCOMPARE( config.enabled, false ); } void TestMerginApi::testHasLocalChangesWithSelectiveSyncEnabled() @@ -3235,10 +3252,10 @@ void TestMerginApi::testHasLocalChangesWithSelectiveSyncEnabled() oldServerFiles.append( serverIncluded ); // retrieve config file we wrote - MerginConfig config = MerginConfig::fromFile( configPath ); + SelectiveSyncConfig config = SelectiveSyncConfig::fromFile( configPath ); QVERIFY( config.isValid ); - QCOMPARE( config.selectiveSyncEnabled, true ); - QCOMPARE( config.selectiveSyncDir, QString( "photos" ) ); // verify selective sync folder + QCOMPARE( config.enabled, true ); + QCOMPARE( config.dir, QString( "photos" ) ); // verify selective sync folder // first scenario => local files list exactly matches the non‑excluded server file // the excluded file ("photos/photo.jpg") is ignored, and no local changes should be detected @@ -3272,7 +3289,7 @@ void TestMerginApi::testHasLocalProjectChanges() QDir dir( projectDir ); QVERIFY( dir.mkdir( ".mergin" ) ); - // 1: first scenario => empty metadata and no local files, selective sync not supported + // 1: first scenario => empty metadata and no local files // create empty metadata MerginProjectMetadata emptyMetadata; QJsonDocument emptyDoc; @@ -3284,13 +3301,11 @@ void TestMerginApi::testHasLocalProjectChanges() emptyDoc.setObject( emptyObj ); writeFileContent( projectDir + "/" + MerginApi::sMetadataFile, emptyDoc.toJson() ); - mApi->setSupportsSelectiveSync( false ); - QVERIFY( !mApi->supportsSelectiveSync() ); // expected results: no changes - QVERIFY( !mApi->hasLocalProjectChanges( projectDir, mApi->supportsSelectiveSync() ) ); + QVERIFY( !MerginApi::hasLocalChanges( projectDir ) ); - // 2: second scenario => metadata has files and no local files, selective sync not supported + // 2: second scenario => metadata has files and no local files // add an entry to metadata file MerginProjectMetadata metadata; QJsonDocument doc; @@ -3310,9 +3325,9 @@ void TestMerginApi::testHasLocalProjectChanges() writeFileContent( projectDir + "/" + MerginApi::sMetadataFile, doc.toJson() ); // expected results: has changes - QVERIFY( mApi->hasLocalProjectChanges( projectDir, mApi->supportsSelectiveSync() ) ); + QVERIFY( MerginApi::hasLocalChanges( projectDir ) ); - // 3: third scenario => metadata files equals local files, selective sync supported + // 3: third scenario => metadata files equals local files writeFileContent( projectDir + "/test.txt", QByteArray( "test content" ) ); // update checksum in metadata file to match local file @@ -3327,19 +3342,16 @@ void TestMerginApi::testHasLocalProjectChanges() doc.setObject( obj ); writeFileContent( projectDir + "/" + MerginApi::sMetadataFile, doc.toJson() ); - mApi->setSupportsSelectiveSync( true ); - QVERIFY( mApi->supportsSelectiveSync() ); - // expected results: no changes - QVERIFY( !mApi->hasLocalProjectChanges( projectDir, mApi->supportsSelectiveSync() ) ); + QVERIFY( !MerginApi::hasLocalChanges( projectDir ) ); - // 4: fourth scenario => local files differs from metadata, selective sync supported + // 4: fourth scenario => local files differs from metadata writeFileContent( projectDir + "/new_file.txt", QByteArray( "new content" ) ); QTest::qSleep( 1000 ); // expected results: has changes - QVERIFY( mApi->hasLocalProjectChanges( projectDir, mApi->supportsSelectiveSync() ) ); + QVERIFY( MerginApi::hasLocalChanges( projectDir ) ); // clean up QDir( projectDir ).removeRecursively(); diff --git a/app/test/testmerginapi.h b/app/test/testmerginapi.h index 44c47fa2c..eede5614d 100644 --- a/app/test/testmerginapi.h +++ b/app/test/testmerginapi.h @@ -148,7 +148,7 @@ class TestMerginApi: public QObject void testAutosync(); void testAutosyncFailure(); void testUpdateProjectMetadataRole(); - void testMerginConfigFromFile(); + void testSelectiveSyncConfigFromFile(); void testHasLocalChangesWithSelectiveSyncEnabled(); void testHasLocalProjectChanges(); void testOfflineCache(); @@ -200,8 +200,8 @@ class TestMerginApi: public QObject 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 &projectNamespace, const QString &projectName, int &serverVersion, bool ignoreSelectiveSync = false ); + void downloadRemoteProject( MerginApi *api, const QString &projectNamespace, const QString &projectName, bool ignoreSelectiveSync = false ); //! 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 );