From 7bfa09476485b45fd778b57c59bc431b9b3293af Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Mon, 22 Dec 2025 18:15:06 +0100 Subject: [PATCH 1/9] fix: detect upgrade only scenario. If no legacy file is found, it means it might be upgrade only. Signed-off-by: Camila Ayres --- src/libsync/configfile.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libsync/configfile.cpp b/src/libsync/configfile.cpp index ea6ca48baeaba..fefc23785c8ac 100644 --- a/src/libsync/configfile.cpp +++ b/src/libsync/configfile.cpp @@ -1352,7 +1352,8 @@ bool ConfigFile::isDowngrade() const bool ConfigFile::shouldTryUnbrandedToBrandedMigration() const { return migrationPhase() == ConfigFile::MigrationPhase::SetupFolders - && Theme::instance()->appName() != unbrandedAppName; + && Theme::instance()->appName() != unbrandedAppName + && !discoveredLegacyConfigPath().isEmpty(); } bool ConfigFile::isUnbrandedToBrandedMigrationInProgress() const From a24ead0df1f14fb676a829c2ac42ca27925e8b0b Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Thu, 4 Dec 2025 22:02:20 +0100 Subject: [PATCH 2/9] refactor: add a class to handle the migration logic. It should cover upgrade, downgrade as well as upgrade from unbranded to branded, from legacy to branded and from legacy to unbranded. Signed-off-by: Camila Ayres --- src/libsync/CMakeLists.txt | 2 + src/libsync/settings/migration.cpp | 130 +++++++++++++++++++++++++++++ src/libsync/settings/migration.h | 67 +++++++++++++++ 3 files changed, 199 insertions(+) create mode 100644 src/libsync/settings/migration.cpp create mode 100644 src/libsync/settings/migration.h diff --git a/src/libsync/CMakeLists.txt b/src/libsync/CMakeLists.txt index 5829eabdcfe70..a2e07c1f2d47f 100644 --- a/src/libsync/CMakeLists.txt +++ b/src/libsync/CMakeLists.txt @@ -164,6 +164,8 @@ set(libsync_SRCS creds/keychainchunk.cpp caseclashconflictsolver.h caseclashconflictsolver.cpp + settings/migration.h + settings/migration.cpp ) if (WIN32) diff --git a/src/libsync/settings/migration.cpp b/src/libsync/settings/migration.cpp new file mode 100644 index 0000000000000..0f175b6997bb1 --- /dev/null +++ b/src/libsync/settings/migration.cpp @@ -0,0 +1,130 @@ +/* + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include + +#include "migration.h" +#include "theme.h" +#include "configfile.h" +#include "version.h" + +namespace OCC { + +Q_LOGGING_CATEGORY(lcMigration, "nextcloud.settings.migration", QtInfoMsg) + +Migration::Migration() +{ + _migrationPhase = MigrationPhase::NotStarted; + _migrationType = MigrationType::UnbrandedToUnbranded; + _versionChangeType = VersionChangeType::NoVersionChange; +} + +QVersionNumber Migration::currentVersion() const +{ + return QVersionNumber::fromString(MIRALL_VERSION_STRING); +} + +QVersionNumber Migration::previousVersion() const +{ + return QVersionNumber::fromString(ConfigFile().clientPreviousVersionString()); +} + +QVersionNumber Migration::configVersion() const +{ + return QVersionNumber::fromString(ConfigFile().clientVersionString()); +} + +void Migration::setMigrationPhase(const MigrationPhase phase) +{ + // do not rollback + if (phase > _migrationPhase) { + _migrationPhase = phase; + } +} + +Migration::MigrationPhase Migration::migrationPhase() const +{ + return _migrationPhase; +} + +void Migration::setMigrationType(const MigrationType type) +{ + _migrationType = type; +} + +Migration::MigrationType Migration::migrationType() const +{ + return _migrationType; +} + +Migration::VersionChangeType Migration::versionChangeType() const +{ + return _versionChangeType; +} + +void Migration::setVersionChangeType(const VersionChangeType type) +{ + _versionChangeType = type; +} + +bool Migration::isUpgrade() +{ + const auto isUpgrade = currentVersion() > previousVersion(); + if (isUpgrade) { + setVersionChangeType(VersionChangeType::Upgrade); + } + return versionChangeType() == VersionChangeType::Upgrade; +} + +bool Migration::isDowngrade() +{ + const auto isDowngrade = previousVersion() > currentVersion(); + if (isDowngrade) { + setVersionChangeType(VersionChangeType::Downgrade); + } + return versionChangeType() == VersionChangeType::Downgrade; +} + +bool Migration::versionChanged() +{ + return isUpgrade() || isDowngrade(); +} + +bool Migration::shouldTryUnbrandedToBrandedMigration() const +{ + const auto isUnbrandedToBranded = migrationPhase() == Migration::MigrationPhase::SetupFolders + && Theme::instance()->appName() != ConfigFile::unbrandedAppName; + if (isUnbrandedToBranded) { + Migration().setMigrationType(MigrationType::UnbrandedToBranded); + } + return migrationType() == MigrationType::UnbrandedToBranded; +} + +bool Migration::isUnbrandedToBrandedMigration() const +{ + return isInProgress() && migrationType() == MigrationType::UnbrandedToBranded; +} + +bool Migration::shouldTryToMigrate() +{ + return !isClientVersionSet() && (isUpgrade() || isDowngrade()); +} + +bool Migration::isClientVersionSet() const +{ + const auto configVersionNumber = configVersion(); + const auto previousVersionNumber = previousVersion(); + return !configVersionNumber.isNull() && !previousVersionNumber.isNull() + && configVersionNumber == previousVersionNumber; +} + +bool Migration::isInProgress() const +{ + const auto currentPhase = migrationPhase(); + return currentPhase != MigrationPhase::NotStarted + && currentPhase != MigrationPhase::Done; +} + +} diff --git a/src/libsync/settings/migration.h b/src/libsync/settings/migration.h new file mode 100644 index 0000000000000..d6f2f85c3e5f4 --- /dev/null +++ b/src/libsync/settings/migration.h @@ -0,0 +1,67 @@ +/* + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef MIGRATION_H +#define MIGRATION_H + +#include +#include "owncloudlib.h" + +namespace OCC { + +class OWNCLOUDSYNC_EXPORT Migration +{ +public: + Migration(); + + enum MigrationPhase { + NotStarted, + SetupConfigFile, + SetupUsers, + SetupFolders, + Done + }; + + enum MigrationType { + UnbrandedToUnbranded, + UnbrandedToBranded, + LegacyToUnbranded, + LegacyToBranded + }; + + enum VersionChangeType { + NoVersionChange, + Upgrade, + Downgrade + }; + + [[nodiscard]] QVersionNumber previousVersion() const; + [[nodiscard]] QVersionNumber currentVersion() const; + [[nodiscard]] QVersionNumber configVersion() const; + + [[nodiscard]] MigrationPhase migrationPhase() const; + [[nodiscard]] MigrationType migrationType() const; + [[nodiscard]] VersionChangeType versionChangeType() const; + + void setMigrationPhase(const MigrationPhase phase); + void setMigrationType(const MigrationType type); + void setVersionChangeType(const VersionChangeType type); + + [[nodiscard]] bool isUpgrade(); + [[nodiscard]] bool isDowngrade(); + [[nodiscard]] bool versionChanged(); + [[nodiscard]] bool shouldTryUnbrandedToBrandedMigration() const; + [[nodiscard]] bool isUnbrandedToBrandedMigration() const; + [[nodiscard]] bool shouldTryToMigrate(); + [[nodiscard]] bool isClientVersionSet() const; + [[nodiscard]] bool isInProgress() const; + +private: + MigrationPhase _migrationPhase; + MigrationType _migrationType; + VersionChangeType _versionChangeType; +}; +} +#endif // MIGRATION_H From d8089d8af6c24667efad06de60931cc48d755072 Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Thu, 4 Dec 2025 22:05:55 +0100 Subject: [PATCH 3/9] refactor: use new migration class. Signed-off-by: Camila Ayres --- src/gui/accountmanager.cpp | 10 ++++-- src/gui/accountstate.cpp | 11 ++++--- src/gui/application.cpp | 23 ++++++++------ src/libsync/configfile.cpp | 63 ++++---------------------------------- src/libsync/configfile.h | 22 ++----------- 5 files changed, 36 insertions(+), 93 deletions(-) diff --git a/src/gui/accountmanager.cpp b/src/gui/accountmanager.cpp index dc3f058f89a33..1aafd2f349283 100644 --- a/src/gui/accountmanager.cpp +++ b/src/gui/accountmanager.cpp @@ -20,6 +20,7 @@ #if !DISABLE_ACCOUNT_MIGRATION #include "legacyaccountselectiondialog.h" #endif +#include "settings/migration.h" #include #include @@ -345,7 +346,8 @@ bool AccountManager::restoreFromLegacySettings() configFile.setDownloadLimit(settings->value(ConfigFile::downloadLimitC, configFile.downloadLimit()).toInt()); // Try to load the single account. - configFile.setMigrationPhase(ConfigFile::MigrationPhase::SetupUsers); + auto migration = configFile.migration(); + migration.setMigrationPhase(Migration::MigrationPhase::SetupUsers); if (!settings->childKeys().isEmpty()) { settings->beginGroup(accountsC); const auto childGroups = selectedAccountIds.isEmpty() ? settings->childGroups() : selectedAccountIds; @@ -545,8 +547,9 @@ void AccountManager::migrateNetworkSettings(const AccountPtr &account, const QSe // Override user settings with global settings if user is set to use global settings ConfigFile configFile; + auto migration = configFile.migration(); auto accountProxySetting = settings.value(networkProxySettingC).toInt(); - if (accountProxySetting == 0 && configFile.isMigrationInProgress()) { + if (accountProxySetting == 0 && migration.isInProgress()) { accountProxyType = static_cast(configFile.proxyType()); accountProxyHost = configFile.proxyHostName(); accountProxyPort = configFile.proxyPort(); @@ -680,8 +683,9 @@ AccountPtr AccountManager::loadAccountHelper(QSettings &settings) acc->setDownloadLimit(settings.value(networkDownloadLimitC).toInt()); ConfigFile configFile; + auto migration = configFile.migration(); const auto proxyPasswordKey = QString(acc->userIdAtHostWithPort() + networkProxyPasswordKeychainKeySuffixC); - const auto appName = configFile.isUnbrandedToBrandedMigrationInProgress() ? ConfigFile::unbrandedAppName + const auto appName = migration.isUnbrandedToBrandedMigration() ? ConfigFile::unbrandedAppName : Theme::instance()->appName(); const auto job = new QKeychain::ReadPasswordJob(appName, this); job->setKey(proxyPasswordKey); diff --git a/src/gui/accountstate.cpp b/src/gui/accountstate.cpp index d139565a56559..4b06ac949524e 100644 --- a/src/gui/accountstate.cpp +++ b/src/gui/accountstate.cpp @@ -17,6 +17,7 @@ #include "ocsuserstatusconnector.h" #include "pushnotifications.h" #include "networkjobs.h" +#include "settings/migration.h" #include #include @@ -297,9 +298,10 @@ void AccountState::checkConnectivity() if (!account()->credentials()->wasFetched()) { _waitingForNewCredentials = true; ConfigFile configFile; - const auto shouldTryUnbrandedToBrandedMigration = configFile.shouldTryUnbrandedToBrandedMigration(); + auto migration = configFile.migration(); + const auto shouldTryUnbrandedToBrandedMigration = migration.shouldTryUnbrandedToBrandedMigration(); qCDebug(lcAccountState) << "shouldTryUnbrandedToBrandedMigration?" << shouldTryUnbrandedToBrandedMigration; - qCDebug(lcAccountState) << "migrationPhase?" << configFile.migrationPhase(); + qCDebug(lcAccountState) << "migrationPhase?" << migration.migrationPhase(); const auto appName = shouldTryUnbrandedToBrandedMigration ? configFile.unbrandedAppName : ""; account()->credentials()->fetchFromKeychain(appName); return; @@ -498,8 +500,9 @@ void AccountState::slotCredentialsFetched(AbstractCredentials *) << "attempting to connect"; _waitingForNewCredentials = false; ConfigFile configFile; - if (configFile.isMigrationInProgress()) { - configFile.setMigrationPhase(ConfigFile::MigrationPhase::Done); + auto migration = configFile.migration(); + if (migration.isInProgress()) { + migration.setMigrationPhase(Migration::MigrationPhase::Done); } checkConnectivity(); } diff --git a/src/gui/application.cpp b/src/gui/application.cpp index bea238ecc0a79..5e891280c054c 100644 --- a/src/gui/application.cpp +++ b/src/gui/application.cpp @@ -39,6 +39,7 @@ #include "common/vfs.h" #include "config.h" +#include "settings/migration.h" #if defined(Q_OS_WIN) #include @@ -121,20 +122,21 @@ namespace { bool Application::configVersionMigration() { ConfigFile configFile; - const auto shouldTryToMigrate = configFile.shouldTryToMigrate(); + Migration migration; + const auto shouldTryToMigrate = migration.shouldTryToMigrate(); if (!shouldTryToMigrate) { qCInfo(lcApplication) << "This is not an upgrade/downgrade/migration. Proceed to read current application config file."; - configFile.setMigrationPhase(ConfigFile::MigrationPhase::Done); + migration.setMigrationPhase(Migration::MigrationPhase::Done); return false; } - configFile.setMigrationPhase(ConfigFile::MigrationPhase::SetupConfigFile); + migration.setMigrationPhase(Migration::MigrationPhase::SetupConfigFile); QStringList deleteKeys, ignoreKeys; AccountManager::backwardMigrationSettingsKeys(&deleteKeys, &ignoreKeys); FolderMan::backwardMigrationSettingsKeys(&deleteKeys, &ignoreKeys); - qCDebug(lcApplication) << "Migration is in progress:" << configFile.isMigrationInProgress(); - const auto versionChanged = configFile.hasVersionChanged(); + qCDebug(lcApplication) << "Migration is in progress:" << migration.isInProgress(); + const auto versionChanged = migration.versionChanged(); if (versionChanged) { qCInfo(lcApplication) << "Version changed. Removing updater settings from config."; configFile.cleanUpdaterConfiguration(); @@ -180,7 +182,7 @@ bool Application::configVersionMigration() "Continuing will mean %2 these settings.
" "
" "The current configuration file was already backed up to %3.") - .arg((configFile.isDowngrade() ? tr("newer", "newer software version") : tr("older", "older software version")), + .arg((Migration().isDowngrade() ? tr("newer", "newer software version") : tr("older", "older software version")), deleteKeys.isEmpty()? tr("ignoring") : tr("deleting"), backupFilesList.join("
"))); box.addButton(tr("Quit"), QMessageBox::AcceptRole); @@ -493,18 +495,18 @@ void Application::setupAccountsAndFolders() { _folderManager.reset(new FolderMan); ConfigFile configFile; - configFile.setMigrationPhase(ConfigFile::MigrationPhase::SetupUsers); + auto migration = configFile.migration(); + migration.setMigrationPhase(Migration::MigrationPhase::SetupUsers); const auto accountsRestoreResult = restoreLegacyAccount(); if (accountsRestoreResult == AccountManager::AccountsNotFound || accountsRestoreResult == AccountManager::AccountsRestoreFailure) { qCWarning(lcApplication) << "Migration result: " << accountsRestoreResult; qCDebug(lcApplication) << "is migration disabled?" << DISABLE_ACCOUNT_MIGRATION; qCWarning(lcApplication) << "No accounts were migrated, prompting user to set up accounts and folders from scratch."; - configFile.setMigrationPhase(ConfigFile::MigrationPhase::Done); - + migration.setMigrationPhase(Migration::MigrationPhase::Done); return; } - configFile.setMigrationPhase(ConfigFile::MigrationPhase::SetupFolders); + migration.setMigrationPhase(Migration::MigrationPhase::SetupFolders); const auto foldersListSize = FolderMan::instance()->setupFolders(); FolderMan::instance()->setSyncEnabled(true); @@ -519,6 +521,7 @@ void Application::setupAccountsAndFolders() const auto accounts = AccountManager::instance()->accounts(); const auto accountsListSize = accounts.size(); if (accountsRestoreResult == AccountManager::AccountsRestoreSuccessFromLegacyVersion + && accountsListSize > 0 && Theme::instance()->displayLegacyImportDialog() && !AccountManager::instance()->forceLegacyImport() && accountsListSize > 0) { diff --git a/src/libsync/configfile.cpp b/src/libsync/configfile.cpp index fefc23785c8ac..ce547181aface 100644 --- a/src/libsync/configfile.cpp +++ b/src/libsync/configfile.cpp @@ -14,6 +14,7 @@ #include "theme.h" #include "updatechannel.h" #include "version.h" +#include "settings/migration.h" #ifndef TOKEN_AUTH_ONLY #include @@ -92,7 +93,7 @@ Q_LOGGING_CATEGORY(lcConfigFile, "nextcloud.sync.configfile", QtInfoMsg) QString ConfigFile::_confDir = {}; QString ConfigFile::_discoveredLegacyConfigPath = {}; -ConfigFile::MigrationPhase ConfigFile::_migrationPhase = ConfigFile::MigrationPhase::NotStarted; +Migration ConfigFile::_migration = Migration{}; static chrono::milliseconds millisecondsValue(const QSettings &setting, const char *key, chrono::milliseconds defaultValue) @@ -327,7 +328,7 @@ void ConfigFile::restoreGeometryHeader(QHeaderView *header) QVariant ConfigFile::getPolicySetting(const QString &setting, const QVariant &defaultValue) const { if (Utility::isWindows()) { - const auto appName = isUnbrandedToBrandedMigrationInProgress() ? unbrandedAppName : Theme::instance()->appNameGUI(); + const auto appName = migration().isUnbrandedToBrandedMigration() ? unbrandedAppName : Theme::instance()->appNameGUI(); // check for policies first and return immediately if a value is found. QSettings userPolicy(QString::fromLatin1(R"(HKEY_CURRENT_USER\Software\Policies\%1\%2)").arg(APPLICATION_VENDOR, appName), QSettings::NativeFormat); @@ -822,7 +823,7 @@ QVariant ConfigFile::getValue(const QString ¶m, const QString &group, const QVariant &defaultValue) const { QVariant systemSetting; - const auto appName = isUnbrandedToBrandedMigrationInProgress() ? unbrandedAppName : Theme::instance()->appNameGUI(); + const auto appName = migration().isUnbrandedToBrandedMigration() ? unbrandedAppName : Theme::instance()->appNameGUI(); if (Utility::isMac()) { QSettings systemSettings(QLatin1String("/Library/Preferences/" APPLICATION_REV_DOMAIN ".plist"), QSettings::NativeFormat); if (!group.isEmpty()) { @@ -1335,60 +1336,8 @@ void ConfigFile::setFileProviderDomainsAppSandboxMigrationCompleted(const bool c settings.setValue(fileProviderDomainsAppSandboxMigrationCompletedC, completed); } -bool ConfigFile::isUpgrade() const -{ - const auto currentVersion = QVersionNumber::fromString(MIRALL_VERSION_STRING); - const auto previousVersion = QVersionNumber::fromString(clientPreviousVersionString()); - return currentVersion > previousVersion; -} - -bool ConfigFile::isDowngrade() const -{ - const auto currentVersion = QVersionNumber::fromString(MIRALL_VERSION_STRING); - const auto previousVersion = QVersionNumber::fromString(clientPreviousVersionString()); - return previousVersion > currentVersion; -} - -bool ConfigFile::shouldTryUnbrandedToBrandedMigration() const -{ - return migrationPhase() == ConfigFile::MigrationPhase::SetupFolders - && Theme::instance()->appName() != unbrandedAppName - && !discoveredLegacyConfigPath().isEmpty(); -} - -bool ConfigFile::isUnbrandedToBrandedMigrationInProgress() const -{ - return isMigrationInProgress() && Theme::instance()->appName() != unbrandedAppName; -} - -bool ConfigFile::shouldTryToMigrate() const -{ - return hasVersionChanged() && (isUpgrade() || isDowngrade()); -} - -bool ConfigFile::hasVersionChanged() const -{ - const auto currentVersion = QVersionNumber::fromString(MIRALL_VERSION_STRING); //app running - const auto clientConfigVersion = QVersionNumber::fromString(clientVersionString()); //config version - return clientConfigVersion != currentVersion; -} - -bool ConfigFile::isMigrationInProgress() const -{ - return _migrationPhase != MigrationPhase::NotStarted && _migrationPhase != MigrationPhase::Done; -} - -void ConfigFile::setMigrationPhase(const MigrationPhase phase) -{ - // do not rollback - if (phase > _migrationPhase) { - _migrationPhase = phase; - } -} - -ConfigFile::MigrationPhase ConfigFile::migrationPhase() const -{ - return _migrationPhase; +Migration &ConfigFile::migration() { + return _migration; } } diff --git a/src/libsync/configfile.h b/src/libsync/configfile.h index 927daa11377e8..f80d832c893b0 100644 --- a/src/libsync/configfile.h +++ b/src/libsync/configfile.h @@ -23,6 +23,7 @@ class ExcludedFiles; namespace OCC { class AbstractCredentials; +class Migration; /** * @brief The ConfigFile class @@ -267,25 +268,8 @@ class OWNCLOUDSYNC_EXPORT ConfigFile /// File Provider app sandbox migration flag [[nodiscard]] bool fileProviderDomainsAppSandboxMigrationCompleted() const; void setFileProviderDomainsAppSandboxMigrationCompleted(bool completed); + [[nodiscard]] static Migration &migration(); - /// Helper function for migration/upgrade proccess - enum MigrationPhase { - NotStarted, - SetupConfigFile, - SetupUsers, - SetupFolders, - Done - }; - [[nodiscard]] bool isUpgrade() const; - [[nodiscard]] bool isDowngrade() const; - [[nodiscard]] bool shouldTryUnbrandedToBrandedMigration() const; - [[nodiscard]] bool isUnbrandedToBrandedMigrationInProgress() const; - [[nodiscard]] bool shouldTryToMigrate() const; - /// Does the current app has a different version of the config version - [[nodiscard]] bool hasVersionChanged() const; - [[nodiscard]] bool isMigrationInProgress() const; - [[nodiscard]] MigrationPhase migrationPhase() const; - void setMigrationPhase(const MigrationPhase phase); static constexpr char unbrandedAppName[] = "Nextcloud"; static constexpr char legacyAppName[] = "Owncloud"; @@ -331,7 +315,7 @@ class OWNCLOUDSYNC_EXPORT ConfigFile static QString _confDir; static QString _discoveredLegacyConfigPath; - static MigrationPhase _migrationPhase; + static Migration _migration; }; } #endif // CONFIGFILE_H From 8db1f3fbaa94ae986ba1cd57530ee8ad1641e410 Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Wed, 30 Apr 2025 22:12:05 +0200 Subject: [PATCH 4/9] test(migration): wip! test each migration scenarion. Signed-off-by: Camila Ayres --- src/gui/folderman.h | 2 + test/CMakeLists.txt | 1 + test/testmigration.cpp | 290 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 293 insertions(+) create mode 100644 test/testmigration.cpp diff --git a/src/gui/folderman.h b/src/gui/folderman.h index e8918a6779180..6bd75da3fb243 100644 --- a/src/gui/folderman.h +++ b/src/gui/folderman.h @@ -28,6 +28,7 @@ class EndToEndTestHelper; class TestSyncConflictsModel; class TestRemoteWipe; class FolderManTestHelper; +class TestMigration; namespace OCC { @@ -418,6 +419,7 @@ private slots: friend class ::TestFolderStatusModel; friend class ::TestRemoteWipe; friend class ::FolderManTestHelper; + friend class ::TestMigration; }; } // namespace OCC diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ee31f8a7a868a..a023df0cce93a 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -149,6 +149,7 @@ nextcloud_add_test(Account) nextcloud_add_test(Folder) nextcloud_add_test(FolderMan) nextcloud_add_test(RemoteWipe) +nextcloud_add_test(Migration) if(NOT BUILD_FILE_PROVIDER_MODULE) # the File Provider build crashes this test in CI for some reason diff --git a/test/testmigration.cpp b/test/testmigration.cpp new file mode 100644 index 0000000000000..ade95db4ea7ec --- /dev/null +++ b/test/testmigration.cpp @@ -0,0 +1,290 @@ +/* + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include +#include +#include +#include + +#include "common/utility.h" +#include "folderman.h" +#include "account.h" +#include "accountstate.h" +#include "accountmanager.h" +#include "configfile.h" +#include "syncenginetestutils.h" +#include "testhelper.h" +#include "version.h" + +using namespace OCC; + +class TestMigration: public QObject +{ + Q_OBJECT + + ConfigFile _configFile; + QTemporaryDir _temporaryDir; + std::unique_ptr _folderMan; + +private: + static constexpr char legacyAppName[] = "ownCloud"; + static constexpr char standardAppName[] = "Nextcloud"; + static constexpr char brandedAppName[] = "Branded"; + static constexpr char ocBrandedAppName[] = "branded"; + static constexpr char legacyAppConfigContent[] = "[General]\n" + "clientVersion=5.3.2.15463\n" + "issuesWidgetFilter=FatalError, BlacklistedError, Excluded, Message, FilenameReserved\n" + "logHttp=false\n" + "optionalDesktopNotifications=true\n" + "\n" + "[Accounts]e\n" + "0\\Folders\\2ba4b09a-1223-aaaa-abcd-c2df238816d8\\davUrl=@Variant(http://oc.de/remote.php/dav/files/admin/)\n" + "0\\Folders\\2ba4b09a-1223-aaaa-abcd-c2df238816d8\\deployed=false\n" + "0\\Folders\\2ba4b09a-1223-aaaa-abcd-c2df238816d8\\displayString=ownCloud\n" + "0\\Folders\2ba4b09a-1223-aaaa-abcd-c2df238816d8\\ignoreHiddenFiles=true\n" + "0\\Folders\2ba4b09a-1223-aaaa-abcd-c2df238816d8\\journalPath=.sync_journal.db\n" + "0\\Folders\2ba4b09a-1223-aaaa-abcd-c2df238816d8\\localPath=/ownCloud/\n" + "0\\Folders\2ba4b09a-1223-aaaa-abcd-c2df238816d8\\paused=false\n" + "0\\Folders\\2ba4b09a-1223-aaaa-abcd-c2df238816d8\\priority=0\n" + "0\\Folders\\2ba4b09a-1223-aaaa-abcd-c2df238816d8\\targetPath=/\n" + "0\\Folders\\2ba4b09a-1223-aaaa-abcd-c2df238816d8\\version=13\n" + "0\\Folders\\2ba4b09a-1223-aaaa-abcd-c2df238816d8\\virtualFilesMode=off\n" + "0\\capabilities=@QVariant()\n" + "0\\dav_user=admin\n" + "0\\default_sync_root=/ownCloud\n" + "0\\display-name=admin\n" + "0\\http_CredentialVersion=1\n" + "0\\http_oauth=false\n" + "0\\http_user=admin\n" + "0\\supportsSpaces=true\n" + "0\\url=http://oc.de/\n" + "0\\user=admin\n" + "0\\userExplicitlySignedOut=false\n" + "0\\uuid=@Variant()\n" + "0\\version=13\n" + "version=13\n" + "\n" + "[Credentials]\n" + "ownCloud_credentials%oc.de%2ba4b09a-1223-aaaa-abcd-c2df238816d8\\http\\password=true"; + +private slots: + void setupStandardConfigFolder() + { + QVERIFY(QDir(_temporaryDir.path()).mkpath(standardAppName)); + const auto standardConfigFolder = QString(_temporaryDir.path() + "/" + standardAppName); + _configFile.setConfDir(standardConfigFolder); + } + + void setupStandarConfig(const QString &version) + { + setupStandardConfigFolder(); + QSettings settings(_configFile.configFile(), QSettings::IniFormat); + _configFile.setClientVersionString(version); + _configFile.setOptionalServerNotifications(true); + _configFile.setShowChatNotifications(true); + _configFile.setShowCallNotifications(true); + _configFile.setShowInExplorerNavigationPane(true); + _configFile.setShowInExplorerNavigationPane(true); + _configFile.setRemotePollInterval(std::chrono::milliseconds(1000)); + _configFile.setAutoUpdateCheck(true, QString()); + _configFile.setUpdateChannel("beta"); + _configFile.setOverrideServerUrl("http://example.de"); + _configFile.setOverrideLocalDir("A"); + _configFile.setVfsEnabled(true); + _configFile.setProxyType(0); + _configFile.setVfsEnabled(true); + _configFile.setUseUploadLimit(0); + _configFile.setUploadLimit(1); + _configFile.setUseDownloadLimit(0); + _configFile.setUseDownloadLimit(1); + _configFile.setNewBigFolderSizeLimit(true, 500); + _configFile.setNotifyExistingFoldersOverLimit(true); + _configFile.setStopSyncingExistingFoldersOverLimit(true); + _configFile.setConfirmExternalStorage(true); + _configFile.setMoveToTrash(true); + _configFile.setForceLoginV2(true); + _configFile.setPromptDeleteFiles(true); + _configFile.setDeleteFilesThreshold(1); + _configFile.setMonoIcons(true); + _configFile.setAutomaticLogDir(true); + _configFile.setLogDir(_temporaryDir.path()); + _configFile.setLogDebug(true); + _configFile.setLogExpire(72); + _configFile.setLogFlush(true); + _configFile.setCertificatePath(_temporaryDir.path()); + _configFile.setCertificatePasswd("123456"); + _configFile.setLaunchOnSystemStartup(true); + _configFile.setServerHasValidSubscription(true); + _configFile.setDesktopEnterpriseChannel("stable"); + _configFile.setLanguage("pt"); + settings.sync(); + QVERIFY(_configFile.exists()); + QScopedPointer fakeQnam(new FakeQNAM({})); + OCC::AccountPtr account = OCC::Account::create(); + account->setDavUser("user"); + account->setDavDisplayName("Nextcloud user"); + account->setProxyType(QNetworkProxy::ProxyType::HttpProxy); + account->setProxyUser("proxyuser"); + account->setDownloadLimit(120); + account->setUploadLimit(120); + account->setDownloadLimitSetting(OCC::Account::AccountNetworkTransferLimitSetting::ManualLimit); + account->setServerVersion("30"); + account->setCredentials(new FakeCredentials{fakeQnam.data()}); + account->setUrl(QUrl(("http://example.de"))); + const auto accountState = OCC::AccountManager::instance()->addAccount(account); + OCC::AccountManager::instance()->saveAccount(accountState->account()); + OCC::FolderDefinition folderDefinition; + folderDefinition.localPath = "/standardAppName"; + folderDefinition.targetPath = "/"; + folderDefinition.alias = standardAppName; + _folderMan.reset({}); + _folderMan.reset(new FolderMan{}); + QVERIFY(_folderMan->addFolder(accountState, folderDefinition)); + } + + + void initTestCase() + { + OCC::Logger::instance()->setLogFlush(true); + OCC::Logger::instance()->setLogDebug(true); + + QStandardPaths::setTestModeEnabled(true); + } + + // Upgrade - TODO: test running app with --confdir + void testUpgrade() + { + // create Nextcloud config with older version + setupStandarConfig("1.0.0"); + const auto oldAppVersionNumber = QVersionNumber::fromString(_configFile.clientVersionString()); + QVERIFY(_configFile.isUpgrade()); + + // backup old config + const auto backupFilesList = _configFile.backupConfigFiles(); + QCOMPARE_GE(backupFilesList.size(), 1); + + // successfully upgrade to new config + const auto afterUpgradeVersionNumber = MIRALL_VERSION_STRING; + _configFile.setClientVersionString(afterUpgradeVersionNumber); + QVERIFY(MIRALL_VERSION_STRING == _configFile.clientVersionString()); + + QCOMPARE_GE(AccountManager::instance()->accounts().size(), 1); + auto accounts = AccountManager::instance()->accounts().first()->settings(); + QCOMPARE_GE(accounts->childGroups().size(), 1); + accounts->beginGroup(QLatin1String("Folders")); + QCOMPARE_GE(accounts->childGroups().size(), 1); + accounts->endGroup(); + } + + // From oC client to Nextcloud + void testMigrationFromOctoNextcloud() + { + QTemporaryDir tempDir; + QVERIFY(QDir(tempDir.path()).mkpath(legacyAppName)); + const auto ocConfigFolder = QString(tempDir.path() + "/" + legacyAppName); + const auto ocConfig = QString(ocConfigFolder + "/" + QString(legacyAppName).toLower() + ".cfg"); + QFile ocConfigFile(ocConfig); + QVERIFY(ocConfigFile.open(QFile::WriteOnly)); + QCOMPARE_GE(ocConfigFile.write(legacyAppConfigContent, qstrlen(legacyAppConfigContent)), 0); + ocConfigFile.close(); + + ConfigFile configFile; + + QVERIFY(QDir(tempDir.path()).mkpath(standardAppName)); + const auto standardConfigFolder = QString(tempDir.path() + "/" + standardAppName); + configFile.setConfDir(standardConfigFolder); + + // Nextcloud config file does not exist + QVERIFY(!configFile.exists()); + + // owncloud config files exists + configFile.findLegacyClientConfigFile(); + const auto legacyConfigFile = configFile.discoveredLegacyConfigFile(); + QVERIFY(!legacyConfigFile.isEmpty()); + QCOMPARE(legacyConfigFile, ocConfig); + + // TODO: add accounts and folders to AccountManager and FolderMan without UI interference + //_folderMan.reset({}); + // _folderMan.reset(new FolderMan{}); + // create accounts and folders from a legacy desktop client or for a new config file + // QVERIFY(AccountManager::instance()->restore(configFile.configFileToRestore()) != AccountManager::AccountsRestoreFailure); + // QCOMPARE_GE(FolderMan::instance()->setupFoldersMigration(), 1); + // QVERIFY(configFile.configFile().contains("nextcloud")); + // QCOMPARE_GE(AccountManager::instance()->accounts().size(), 1); + // auto accounts = AccountManager::instance()->accounts().first()->settings(); + // QCOMPARE_GE(accounts->childGroups().size(), 1); + // accounts->beginGroup(QLatin1String("Folders")); + // QCOMPARE_GE(accounts->childGroups().size(), 1); + // accounts->endGroup(); + } + + // From branded oC client to branded Nextcloud + void testMigrationFromBrandedOctoBrandedNextcloud() + { + QCoreApplication::setApplicationName(brandedAppName); + setupStandardConfigFolder(); + + // branded legacy have directory name in lower case + QTemporaryDir tempDir; + QVERIFY(QDir(tempDir.path()).mkpath(ocBrandedAppName)); + const auto ocBrandedConfigFolder = QString(tempDir.path() + "/" + ocBrandedAppName); + const auto ocBrandedConfig = QString(ocBrandedConfigFolder + "/" + QString(ocBrandedAppName) + ".cfg"); + QFile::copy(_configFile.configFile(), QFileInfo(ocBrandedConfig).filePath()); + + QFile ocBrandedConfigFile(ocBrandedConfig); + QVERIFY(ocBrandedConfigFile.open(QFile::WriteOnly)); + QCOMPARE_GE(ocBrandedConfigFile.write(legacyAppConfigContent, qstrlen(legacyAppConfigContent)), 0); + ocBrandedConfigFile.close(); + + ConfigFile configFile; + QVERIFY(QDir(tempDir.path()).mkpath(brandedAppName)); + const auto brandedConfigFolder = QString(tempDir.path() + "/" + brandedAppName); + configFile.setConfDir(brandedConfigFolder); + + const auto path3 = _configFile.configFile(); + const auto path4 = configFile.configFile(); + + // our branded config file does not exist + QVERIFY(!configFile.exists()); + + // branded owncloud config files exists + configFile.findLegacyClientConfigFile(); + const auto legacyConfigFile = configFile.discoveredLegacyConfigFile(); + QVERIFY(!legacyConfigFile.isEmpty()); + QCOMPARE(legacyConfigFile, ocBrandedConfig); + } + + + // From the standard Nextcloud client to a branded version + void testMigrationFromNextcloudToBranded() + { + setupStandardConfigFolder(); + + QTemporaryDir tempDir; + QVERIFY(QDir(tempDir.path()).mkpath(brandedAppName)); + const auto brandedConfigFolder = QString(tempDir.path() + "/" + brandedAppName); + const auto brandedConfig = QString(brandedConfigFolder + "/" + QString(brandedAppName).toLower() + ".cfg"); + QFile::copy(_configFile.configFile(), QFileInfo(brandedConfig).filePath()); + + ConfigFile configFile; + QVERIFY(QDir(tempDir.path()).mkpath(standardAppName)); + const auto standardConfigFolder = QString(tempDir.path() + "/" + standardAppName); + configFile.setConfDir(standardConfigFolder); + + // Nextcloud config file does not exist + QVERIFY(!configFile.exists()); + + // owncloud config files exists + configFile.findLegacyClientConfigFile(); + const auto legacyConfigFile = configFile.discoveredLegacyConfigFile(); + QVERIFY(!legacyConfigFile.isEmpty()); + QCOMPARE(legacyConfigFile, brandedConfig); + } + + // TODO: Downgrade +}; + +QTEST_GUILESS_MAIN(TestMigration) +#include "testmigration.moc" From e0699137a290aeed7943db24925cdfb2e1dabfd0 Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Wed, 7 Jan 2026 10:48:33 +0100 Subject: [PATCH 5/9] fix: detect upgrade only scenario. If no legacy file is found, it means it might be upgrade only. Signed-off-by: Camila Ayres --- src/libsync/settings/migration.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libsync/settings/migration.cpp b/src/libsync/settings/migration.cpp index 0f175b6997bb1..b4997eacef20b 100644 --- a/src/libsync/settings/migration.cpp +++ b/src/libsync/settings/migration.cpp @@ -95,7 +95,9 @@ bool Migration::versionChanged() bool Migration::shouldTryUnbrandedToBrandedMigration() const { const auto isUnbrandedToBranded = migrationPhase() == Migration::MigrationPhase::SetupFolders - && Theme::instance()->appName() != ConfigFile::unbrandedAppName; + && Theme::instance()->appName() != ConfigFile::unbrandedAppName + && !ConfigFile().discoveredLegacyConfigPath().isEmpty(); + if (isUnbrandedToBranded) { Migration().setMigrationType(MigrationType::UnbrandedToBranded); } From cac1a38ded2e8e8bbfe98b988943449b9b9cf0e7 Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Thu, 8 Jan 2026 13:08:34 +0100 Subject: [PATCH 6/9] refactor: move backup config files logic from Application to ConfigFile. Signed-off-by: Camila Ayres --- src/gui/application.cpp | 22 +++--------------- src/libsync/configfile.cpp | 46 +++++++++++++++++++++++++------------- src/libsync/configfile.h | 7 ++---- 3 files changed, 35 insertions(+), 40 deletions(-) diff --git a/src/gui/application.cpp b/src/gui/application.cpp index 5e891280c054c..4f6ccec3134c5 100644 --- a/src/gui/application.cpp +++ b/src/gui/application.cpp @@ -154,25 +154,9 @@ bool Application::configVersionMigration() // default is now off to displaying dialog warning user of too many files deletion configFile.setPromptDeleteFiles(false); - // back up all old config files - QStringList backupFilesList; - QDir configDir(configFile.configPath()); - const auto anyConfigFileNameList = configDir.entryInfoList({"*.cfg"}, QDir::Files); - for (const auto &oldConfig : anyConfigFileNameList) { - const auto oldConfigFileName = oldConfig.fileName(); - const auto oldConfigFilePath = oldConfig.filePath(); - const auto newConfigFileName = configFile.configFile(); - backupFilesList.append(configFile.backup(oldConfigFileName)); - if (oldConfigFilePath != newConfigFileName) { - if (!QFile::rename(oldConfigFilePath, newConfigFileName)) { - qCWarning(lcApplication) << "Failed to rename configuration file from" << oldConfigFilePath << "to" << newConfigFileName; - } - } - } - - // We want to message the user either for destructive changes, + // back up all old config files and message the user either for destructive changes, // or if we're ignoring something and the client version changed. - if (configFile.showConfigBackupWarning() && backupFilesList.count() > 0) { + if (const auto backupFilesList = configFile.backupConfigFiles(); configFile.showConfigBackupWarning() && backupFilesList.count() > 0) { QMessageBox box( QMessageBox::Warning, APPLICATION_SHORTNAME, @@ -182,7 +166,7 @@ bool Application::configVersionMigration() "Continuing will mean %2 these settings.
" "
" "The current configuration file was already backed up to %3.") - .arg((Migration().isDowngrade() ? tr("newer", "newer software version") : tr("older", "older software version")), + .arg((configFile.migration().isDowngrade() ? tr("newer", "newer software version") : tr("older", "older software version")), deleteKeys.isEmpty()? tr("ignoring") : tr("deleting"), backupFilesList.join("
"))); box.addButton(tr("Quit"), QMessageBox::AcceptRole); diff --git a/src/libsync/configfile.cpp b/src/libsync/configfile.cpp index ce547181aface..bab933fcbedf2 100644 --- a/src/libsync/configfile.cpp +++ b/src/libsync/configfile.cpp @@ -92,7 +92,6 @@ namespace chrono = std::chrono; Q_LOGGING_CATEGORY(lcConfigFile, "nextcloud.sync.configfile", QtInfoMsg) QString ConfigFile::_confDir = {}; -QString ConfigFile::_discoveredLegacyConfigPath = {}; Migration ConfigFile::_migration = Migration{}; static chrono::milliseconds millisecondsValue(const QSettings &setting, const char *key, @@ -382,7 +381,7 @@ QString ConfigFile::excludeFile(Scope scope) const return ConfigFile::excludeFileFromSystem(); } - const auto excludeFilePath = scope == LegacyScope ? discoveredLegacyConfigPath() : configPath(); + const auto excludeFilePath = scope == LegacyScope ? _migration.discoveredLegacyConfigPath() : configPath(); // prefer sync-exclude.lst, but if it does not exist, check for exclude.lst QFileInfo exclFileInfo(excludeFilePath, syncExclFile); @@ -1302,20 +1301,6 @@ void ConfigFile::setupDefaultExcludeFilePaths(ExcludedFiles &excludedFiles) excludedFiles.addExcludeFilePath(userList); } -QString ConfigFile::discoveredLegacyConfigPath() -{ - return _discoveredLegacyConfigPath; -} - -void ConfigFile::setDiscoveredLegacyConfigPath(const QString &discoveredLegacyConfigPath) -{ - if (_discoveredLegacyConfigPath == discoveredLegacyConfigPath) { - return; - } - - _discoveredLegacyConfigPath = discoveredLegacyConfigPath; -} - void ConfigFile::removeFileProviderDomainMapping() { QSettings settings(configFile(), QSettings::IniFormat); @@ -1336,6 +1321,35 @@ void ConfigFile::setFileProviderDomainsAppSandboxMigrationCompleted(const bool c settings.setValue(fileProviderDomainsAppSandboxMigrationCompletedC, completed); } +QStringList ConfigFile::backupConfigFiles() const +{ + // 'Launch on system startup' defaults to true > 3.11.x + const auto theme = Theme::instance(); + ConfigFile().setLaunchOnSystemStartup(ConfigFile().launchOnSystemStartup()); + Utility::setLaunchOnStartup(theme->appName(), theme->appNameGUI(), ConfigFile().launchOnSystemStartup()); + + // default is now off to displaying dialog warning user of too many files deletion + ConfigFile().setPromptDeleteFiles(false); + + // back up all old config files + QStringList backupFilesList; + QDir configDir(ConfigFile().configPath()); + const auto anyConfigFileNameList = configDir.entryInfoList({"*.cfg"}, QDir::Files); + for (const auto &oldConfig : anyConfigFileNameList) { + const auto oldConfigFileName = oldConfig.fileName(); + const auto oldConfigFilePath = oldConfig.filePath(); + const auto newConfigFileName = ConfigFile().configFile(); + backupFilesList.append(backup(oldConfigFileName)); + if (oldConfigFilePath != newConfigFileName) { + if (!QFile::rename(oldConfigFilePath, newConfigFileName)) { + qCWarning(lcConfigFile) << "Failed to rename configuration file from" << oldConfigFilePath << "to" << newConfigFileName; + } + } + } + + return backupFilesList; +} + Migration &ConfigFile::migration() { return _migration; } diff --git a/src/libsync/configfile.h b/src/libsync/configfile.h index f80d832c893b0..65894a9eadcae 100644 --- a/src/libsync/configfile.h +++ b/src/libsync/configfile.h @@ -254,10 +254,6 @@ class OWNCLOUDSYNC_EXPORT ConfigFile /// Add the system and user exclude file path to the ExcludedFiles instance. static void setupDefaultExcludeFilePaths(ExcludedFiles &excludedFiles); - /// Set during first time migration of legacy accounts in AccountManager - [[nodiscard]] static QString discoveredLegacyConfigPath(); - static void setDiscoveredLegacyConfigPath(const QString &discoveredLegacyConfigPath); - /// File Provider Domain UUID to Account ID mapping /** @@ -268,7 +264,9 @@ class OWNCLOUDSYNC_EXPORT ConfigFile /// File Provider app sandbox migration flag [[nodiscard]] bool fileProviderDomainsAppSandboxMigrationCompleted() const; void setFileProviderDomainsAppSandboxMigrationCompleted(bool completed); + [[nodiscard]] static Migration &migration(); + [[nodiscard]] QStringList backupConfigFiles() const; static constexpr char unbrandedAppName[] = "Nextcloud"; static constexpr char legacyAppName[] = "Owncloud"; @@ -314,7 +312,6 @@ class OWNCLOUDSYNC_EXPORT ConfigFile using SharedCreds = QSharedPointer; static QString _confDir; - static QString _discoveredLegacyConfigPath; static Migration _migration; }; } From 8f1e6bf49baae07c659f80197cb4ac64922e9a8c Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Thu, 8 Jan 2026 13:11:04 +0100 Subject: [PATCH 7/9] refactor: find and readlegacy config files in the Migration class. Signed-off-by: Camila Ayres --- src/gui/accountmanager.cpp | 147 ++++++++++------------------- src/gui/folderman.cpp | 3 +- src/gui/folderman.h | 1 + src/libsync/settings/migration.cpp | 98 ++++++++++++++++++- src/libsync/settings/migration.h | 22 ++++- 5 files changed, 169 insertions(+), 102 deletions(-) diff --git a/src/gui/accountmanager.cpp b/src/gui/accountmanager.cpp index 1aafd2f349283..f14a9b7faf2e8 100644 --- a/src/gui/accountmanager.cpp +++ b/src/gui/accountmanager.cpp @@ -87,7 +87,6 @@ constexpr auto serverDesktopEnterpriseUpdateChannelC = "desktopEnterpriseChannel constexpr auto generalC = "General"; } - namespace OCC { Q_LOGGING_CATEGORY(lcAccountManager, "nextcloud.gui.account.manager", QtInfoMsg) @@ -197,110 +196,64 @@ bool AccountManager::restoreFromLegacySettings() { qCInfo(lcAccountManager) << "Migrate: restoreFromLegacySettings, checking settings group" << Theme::instance()->appName(); - // try to open the correctly themed settings auto settings = ConfigFile::settingsWithGroup(Theme::instance()->appName()); - auto wasLegacyImportDialogDisplayed = false; - const auto displayLegacyImportDialog = Theme::instance()->displayLegacyImportDialog(); QStringList selectedAccountIds; - - // if the settings file could not be opened, the childKeys list is empty - // then try to load settings from a very old place - if (settings->childKeys().isEmpty()) { - // Legacy settings used QDesktopServices to get the location for the config folder in 2.4 and before - const auto legacy2_4CfgSettingsLocation = QString(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/data")); - const auto legacy2_4CfgFileParentFolder = legacy2_4CfgSettingsLocation.left(legacy2_4CfgSettingsLocation.lastIndexOf('/')); - - // 2.5+ (rest of 2.x series) - const auto legacy2_5CfgSettingsLocation = QStandardPaths::writableLocation(Utility::isWindows() ? QStandardPaths::AppDataLocation : QStandardPaths::AppConfigLocation); - const auto legacy2_5CfgFileParentFolder = legacy2_5CfgSettingsLocation.left(legacy2_5CfgSettingsLocation.lastIndexOf('/')); - - // Now try the locations we use today - const auto fullLegacyCfgFile = QDir::fromNativeSeparators(settings->fileName()); - const auto legacyCfgFileParentFolder = fullLegacyCfgFile.left(fullLegacyCfgFile.lastIndexOf('/')); - const auto legacyCfgFileGrandParentFolder = legacyCfgFileParentFolder.left(legacyCfgFileParentFolder.lastIndexOf('/')); - - const auto legacyCfgFileNamePath = QString(QStringLiteral("/") + legacyCfgFileNameC); - const auto legacyCfgFileRelativePath = QString(legacyRelativeConfigLocationC); - - auto legacyLocations = QVector{legacy2_4CfgFileParentFolder + legacyCfgFileRelativePath, - legacy2_5CfgFileParentFolder + legacyCfgFileRelativePath, - legacyCfgFileParentFolder + legacyCfgFileNamePath, - legacyCfgFileGrandParentFolder + legacyCfgFileRelativePath}; - - if (Theme::instance()->isBranded()) { - const auto unbrandedCfgFileNamePath = QString(QStringLiteral("/") + unbrandedCfgFileNameC); - const auto unbrandedCfgFileRelativePath = QString(unbrandedRelativeConfigLocationC); - legacyLocations.append({legacyCfgFileParentFolder + unbrandedCfgFileNamePath, legacyCfgFileGrandParentFolder + unbrandedCfgFileRelativePath}); - } - - for (const auto &configFile : std::as_const(legacyLocations)) { - auto oCSettings = std::make_unique(configFile, QSettings::IniFormat); - if (oCSettings->status() != QSettings::Status::NoError) { - qCInfo(lcAccountManager) << "Error reading legacy configuration file" << oCSettings->status(); - break; - } - - oCSettings->beginGroup(QLatin1String(accountsC)); - const auto childGroups = oCSettings->childGroups(); - const auto accountsListSize = childGroups.size(); - oCSettings->endGroup(); //accountsC - if (const QFileInfo configFileInfo(configFile); - configFileInfo.exists() && configFileInfo.isReadable()) { - - qCInfo(lcAccountManager) << "Migrate: checking old config " << configFile; - if (!forceLegacyImport() && accountsListSize > 0 && displayLegacyImportDialog) { - wasLegacyImportDialogDisplayed = true; - if (accountsListSize == 1) { - const auto importQuestion = - tr("An account was detected from a legacy desktop client.\n" - "Should the account be imported?"); - QMessageBox importMessageBox(QMessageBox::Question, tr("Legacy import"), importQuestion); - importMessageBox.addButton(tr("Import"), QMessageBox::AcceptRole); - const auto skipButton = importMessageBox.addButton(tr("Skip"), QMessageBox::DestructiveRole); - importMessageBox.exec(); - if (importMessageBox.clickedButton() == skipButton) { - return false; - } - selectedAccountIds = childGroups; - } else { - QVector accountsToDisplay; - oCSettings->beginGroup(QLatin1String(accountsC)); - for (const auto &accId : childGroups) { - oCSettings->beginGroup(accId); - const auto displayName = oCSettings->value(QLatin1String(displayNameC)).toString(); - const auto urlStr = oCSettings->value(QLatin1String(urlC)).toString(); - oCSettings->endGroup(); //accId - const auto label = QString("%1 - %2").arg(displayName, urlStr); - accountsToDisplay.push_back({accId, label}); - } - oCSettings->endGroup(); //accountsC - - LegacyAccountSelectionDialog accountSelectionDialog(accountsToDisplay); - if (accountSelectionDialog.exec() != QDialog::Accepted) { - return false; - } - selectedAccountIds = accountSelectionDialog.selectedAccountIds(); - if (selectedAccountIds.isEmpty()) { - return false; - } - } - } else { - selectedAccountIds = childGroups; + if (const auto legacyData = ConfigFile().migration().legacyData(); !legacyData.configFile.isEmpty()) { + + const auto displayLegacyImportDialog = Theme::instance()->displayLegacyImportDialog(); + + const auto configFile = legacyData.configFile; + auto oCSettings = std::move(legacyData.settings); + + oCSettings->beginGroup(QLatin1String(accountsC)); + const auto childGroups = oCSettings->childGroups(); + const auto accountsListSize = childGroups.size(); + oCSettings->endGroup(); // accountsC + + qCInfo(lcAccountManager) << "Migrate: checking old config " << configFile; + if (!forceLegacyImport() && displayLegacyImportDialog && accountsListSize > 0) { + wasLegacyImportDialogDisplayed = true; + if (childGroups.size() == 1) { + const auto importQuestion = + tr("An account was detected from a legacy desktop client.\n" + "Should the account be imported?"); + QMessageBox importMessageBox(QMessageBox::Question, tr("Legacy import"), importQuestion); + importMessageBox.addButton(tr("Import"), QMessageBox::AcceptRole); + const auto skipButton = importMessageBox.addButton(tr("Skip"), QMessageBox::DestructiveRole); + importMessageBox.exec(); + if (importMessageBox.clickedButton() == skipButton) { + return false; } - - const auto legacyVersion = oCSettings->value(ConfigFile::clientVersionC, {}).toString(); - ConfigFile().setClientPreviousVersionString(legacyVersion); - qCInfo(lcAccountManager) << "Migrating from" << legacyVersion; - qCInfo(lcAccountManager) << "Copy settings" << oCSettings->allKeys().join(", "); - settings = std::move(oCSettings); - ConfigFile::setDiscoveredLegacyConfigPath(configFileInfo.canonicalPath()); - break; + selectedAccountIds = childGroups; } else { - qCInfo(lcAccountManager) << "Migrate: could not read old config " << configFile; + QVector accountsToDisplay; + oCSettings->beginGroup(QLatin1String(accountsC)); + for (const auto &accId : childGroups) { + oCSettings->beginGroup(accId); + const auto displayName = oCSettings->value(QLatin1String(displayNameC)).toString(); + const auto urlStr = oCSettings->value(QLatin1String(urlC)).toString(); + oCSettings->endGroup(); // accId + const auto label = QString("%1 - %2").arg(displayName, urlStr); + accountsToDisplay.push_back({accId, label}); + } + oCSettings->endGroup(); // accountsC + + LegacyAccountSelectionDialog accountSelectionDialog(accountsToDisplay); + if (accountSelectionDialog.exec() != QDialog::Accepted) { + return false; + } + selectedAccountIds = accountSelectionDialog.selectedAccountIds(); + if (selectedAccountIds.isEmpty()) { + return false; + } } + } else { + selectedAccountIds = childGroups; } + + settings.reset(oCSettings.get()); } ConfigFile configFile; diff --git a/src/gui/folderman.cpp b/src/gui/folderman.cpp index 54eb15e53b24d..9967368762bb2 100644 --- a/src/gui/folderman.cpp +++ b/src/gui/folderman.cpp @@ -20,6 +20,7 @@ #include #include #include "updatee2eefolderusersmetadatajob.h" +#include "settings/migration.h" #ifdef Q_OS_MACOS #include @@ -351,7 +352,7 @@ int FolderMan::setupFoldersMigration() auto configPath = _folderConfigPath; #if !DISABLE_ACCOUNT_MIGRATION - if (const auto legacyConfigPath = ConfigFile::discoveredLegacyConfigPath();!legacyConfigPath.isEmpty()) { + if (const auto legacyConfigPath = cfg.migration().discoveredLegacyConfigPath(); !legacyConfigPath.isEmpty()) { configPath = legacyConfigPath; qCInfo(lcFolderMan) << "Starting folder migration from legacy path:" << legacyConfigPath; } diff --git a/src/gui/folderman.h b/src/gui/folderman.h index 6bd75da3fb243..7ff8ebaef1bd8 100644 --- a/src/gui/folderman.h +++ b/src/gui/folderman.h @@ -37,6 +37,7 @@ class SyncResult; class SocketApi; class LockWatcher; class UpdateE2eeFolderUsersMetadataJob; +class Migration; /** * @brief The FolderMan class diff --git a/src/libsync/settings/migration.cpp b/src/libsync/settings/migration.cpp index b4997eacef20b..0f4b8ac2b3e4e 100644 --- a/src/libsync/settings/migration.cpp +++ b/src/libsync/settings/migration.cpp @@ -9,6 +9,19 @@ #include "theme.h" #include "configfile.h" #include "version.h" +#include "common/utility.h" + +#include +#include +#include + +namespace { + constexpr auto accountsC = "Accounts"; + constexpr auto legacyCfgFileNameC = "owncloud.cfg"; + constexpr auto legacyRelativeConfigLocationC = "/ownCloud/owncloud.cfg"; + constexpr auto unbrandedRelativeConfigLocationC = "/Nextcloud/nextcloud.cfg"; + constexpr auto unbrandedCfgFileNameC = "nextcloud.cfg"; +} namespace OCC { @@ -19,6 +32,7 @@ Migration::Migration() _migrationPhase = MigrationPhase::NotStarted; _migrationType = MigrationType::UnbrandedToUnbranded; _versionChangeType = VersionChangeType::NoVersionChange; + _discoveredLegacyConfigPath = {}; } QVersionNumber Migration::currentVersion() const @@ -96,7 +110,7 @@ bool Migration::shouldTryUnbrandedToBrandedMigration() const { const auto isUnbrandedToBranded = migrationPhase() == Migration::MigrationPhase::SetupFolders && Theme::instance()->appName() != ConfigFile::unbrandedAppName - && !ConfigFile().discoveredLegacyConfigPath().isEmpty(); + && !_discoveredLegacyConfigPath.isEmpty(); if (isUnbrandedToBranded) { Migration().setMigrationType(MigrationType::UnbrandedToBranded); @@ -129,4 +143,86 @@ bool Migration::isInProgress() const && currentPhase != MigrationPhase::Done; } +Migration::LegacyData Migration::legacyData() const +{ + qCInfo(lcMigration) << "Migrate: restoreFromLegacySettings, checking settings group" << Theme::instance()->appName(); + + // try to open the correctly themed settings + auto settings = ConfigFile::settingsWithGroup(Theme::instance()->appName()); + LegacyData legacyData; + + // if the settings file could not be opened, the childKeys list is empty + // then try to load settings from a very old place + if (settings->childKeys().isEmpty()) { + // Legacy settings used QDesktopServices to get the location for the config folder in 2.4 and before + const auto legacy2_4CfgSettingsLocation = QString(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/data")); + const auto legacy2_4CfgFileParentFolder = legacy2_4CfgSettingsLocation.left(legacy2_4CfgSettingsLocation.lastIndexOf('/')); + + // 2.5+ (rest of 2.x series) + const auto legacy2_5CfgSettingsLocation = + QStandardPaths::writableLocation(Utility::isWindows() ? QStandardPaths::AppDataLocation : QStandardPaths::AppConfigLocation); + const auto legacy2_5CfgFileParentFolder = legacy2_5CfgSettingsLocation.left(legacy2_5CfgSettingsLocation.lastIndexOf('/')); + + // Now try the locations we use today + const auto fullLegacyCfgFile = QDir::fromNativeSeparators(settings->fileName()); + const auto legacyCfgFileParentFolder = fullLegacyCfgFile.left(fullLegacyCfgFile.lastIndexOf('/')); + const auto legacyCfgFileGrandParentFolder = legacyCfgFileParentFolder.left(legacyCfgFileParentFolder.lastIndexOf('/')); + + const auto legacyCfgFileNamePath = QString(QStringLiteral("/") + legacyCfgFileNameC); + const auto legacyCfgFileRelativePath = QString(legacyRelativeConfigLocationC); + + auto legacyLocations = QVector{legacy2_4CfgFileParentFolder + legacyCfgFileRelativePath, + legacy2_5CfgFileParentFolder + legacyCfgFileRelativePath, + legacyCfgFileParentFolder + legacyCfgFileNamePath, + legacyCfgFileGrandParentFolder + legacyCfgFileRelativePath}; + + if (Theme::instance()->isBranded()) { + const auto unbrandedCfgFileNamePath = QString(QStringLiteral("/") + unbrandedCfgFileNameC); + const auto unbrandedCfgFileRelativePath = QString(unbrandedRelativeConfigLocationC); + legacyLocations.append({legacyCfgFileParentFolder + unbrandedCfgFileNamePath, legacyCfgFileGrandParentFolder + unbrandedCfgFileRelativePath}); + } + + for (const auto &configFileString : std::as_const(legacyLocations)) { + auto oCSettings = std::make_unique(configFileString, QSettings::IniFormat); + if (oCSettings->status() != QSettings::Status::NoError) { + qCInfo(lcMigration) << "Error reading legacy configuration file" << oCSettings->status(); + break; + } + + if (const QFileInfo configFileInfo(configFileString); configFileInfo.exists() && configFileInfo.isReadable()) { + ConfigFile configFile; + const auto legacyVersion = oCSettings->value(ConfigFile::clientVersionC, {}).toString(); + configFile.setClientPreviousVersionString(legacyVersion); + qCInfo(lcMigration) << "Migrating from" << legacyVersion; + qCInfo(lcMigration) << "Copy settings" << oCSettings->allKeys().join(", "); + Migration().setDiscoveredLegacyConfigPath(configFileInfo.canonicalPath()); + + + legacyData.configFile = configFileString; + legacyData.settings.create(oCSettings.release()); + + break; + } else { + qCInfo(lcMigration) << "Migrate: could not read old config " << configFileString; + } + } + } + + return legacyData; +} + +QString Migration::discoveredLegacyConfigPath() const +{ + return _discoveredLegacyConfigPath; +} + +void Migration::setDiscoveredLegacyConfigPath(const QString &discoveredLegacyConfigPath) +{ + if (_discoveredLegacyConfigPath == discoveredLegacyConfigPath) { + return; + } + + _discoveredLegacyConfigPath = discoveredLegacyConfigPath; +} + } diff --git a/src/libsync/settings/migration.h b/src/libsync/settings/migration.h index d6f2f85c3e5f4..967486947dd4b 100644 --- a/src/libsync/settings/migration.h +++ b/src/libsync/settings/migration.h @@ -7,6 +7,8 @@ #define MIGRATION_H #include +#include +#include #include "owncloudlib.h" namespace OCC { @@ -37,18 +39,30 @@ class OWNCLOUDSYNC_EXPORT Migration Downgrade }; + struct LegacyData { + QString configFile; + QSharedPointer settings; + }; + [[nodiscard]] QVersionNumber previousVersion() const; [[nodiscard]] QVersionNumber currentVersion() const; [[nodiscard]] QVersionNumber configVersion() const; [[nodiscard]] MigrationPhase migrationPhase() const; - [[nodiscard]] MigrationType migrationType() const; - [[nodiscard]] VersionChangeType versionChangeType() const; - void setMigrationPhase(const MigrationPhase phase); + + [[nodiscard]] MigrationType migrationType() const; void setMigrationType(const MigrationType type); + + [[nodiscard]] VersionChangeType versionChangeType() const; void setVersionChangeType(const VersionChangeType type); + [[nodiscard]] LegacyData legacyData() const; + + /// Set during first time migration of legacy accounts in AccountManager + [[nodiscard]] QString discoveredLegacyConfigPath() const; + void setDiscoveredLegacyConfigPath(const QString &discoveredLegacyConfigPath); + [[nodiscard]] bool isUpgrade(); [[nodiscard]] bool isDowngrade(); [[nodiscard]] bool versionChanged(); @@ -62,6 +76,8 @@ class OWNCLOUDSYNC_EXPORT Migration MigrationPhase _migrationPhase; MigrationType _migrationType; VersionChangeType _versionChangeType; + QString _discoveredLegacyConfigPath; + LegacyData _configSettings; }; } #endif // MIGRATION_H From 3e9c7619fc02d95fb642a8c1778e703eb5f887b0 Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Thu, 8 Jan 2026 17:17:38 +0100 Subject: [PATCH 8/9] refactor: make all Migration class members static. Signed-off-by: Camila Ayres --- src/gui/accountmanager.cpp | 8 ++++---- src/gui/accountstate.cpp | 4 ++-- src/gui/application.cpp | 4 ++-- src/gui/folderman.cpp | 3 ++- src/libsync/configfile.cpp | 16 ++++++++-------- src/libsync/configfile.h | 2 -- src/libsync/settings/migration.cpp | 25 ++++++++++++++----------- src/libsync/settings/migration.h | 13 +++++++------ test/testmigration.cpp | 19 ++++++++++++------- 9 files changed, 51 insertions(+), 43 deletions(-) diff --git a/src/gui/accountmanager.cpp b/src/gui/accountmanager.cpp index f14a9b7faf2e8..ab7c7cc03161a 100644 --- a/src/gui/accountmanager.cpp +++ b/src/gui/accountmanager.cpp @@ -200,7 +200,8 @@ bool AccountManager::restoreFromLegacySettings() auto settings = ConfigFile::settingsWithGroup(Theme::instance()->appName()); auto wasLegacyImportDialogDisplayed = false; QStringList selectedAccountIds; - if (const auto legacyData = ConfigFile().migration().legacyData(); !legacyData.configFile.isEmpty()) { + Migration migration; + if (const auto legacyData = migration.legacyData(); !legacyData.configFile.isEmpty()) { const auto displayLegacyImportDialog = Theme::instance()->displayLegacyImportDialog(); @@ -299,7 +300,6 @@ bool AccountManager::restoreFromLegacySettings() configFile.setDownloadLimit(settings->value(ConfigFile::downloadLimitC, configFile.downloadLimit()).toInt()); // Try to load the single account. - auto migration = configFile.migration(); migration.setMigrationPhase(Migration::MigrationPhase::SetupUsers); if (!settings->childKeys().isEmpty()) { settings->beginGroup(accountsC); @@ -500,7 +500,7 @@ void AccountManager::migrateNetworkSettings(const AccountPtr &account, const QSe // Override user settings with global settings if user is set to use global settings ConfigFile configFile; - auto migration = configFile.migration(); + Migration migration; auto accountProxySetting = settings.value(networkProxySettingC).toInt(); if (accountProxySetting == 0 && migration.isInProgress()) { accountProxyType = static_cast(configFile.proxyType()); @@ -636,7 +636,7 @@ AccountPtr AccountManager::loadAccountHelper(QSettings &settings) acc->setDownloadLimit(settings.value(networkDownloadLimitC).toInt()); ConfigFile configFile; - auto migration = configFile.migration(); + Migration migration; const auto proxyPasswordKey = QString(acc->userIdAtHostWithPort() + networkProxyPasswordKeychainKeySuffixC); const auto appName = migration.isUnbrandedToBrandedMigration() ? ConfigFile::unbrandedAppName : Theme::instance()->appName(); diff --git a/src/gui/accountstate.cpp b/src/gui/accountstate.cpp index 4b06ac949524e..f1a4f88b3f5d7 100644 --- a/src/gui/accountstate.cpp +++ b/src/gui/accountstate.cpp @@ -298,7 +298,7 @@ void AccountState::checkConnectivity() if (!account()->credentials()->wasFetched()) { _waitingForNewCredentials = true; ConfigFile configFile; - auto migration = configFile.migration(); + Migration migration; const auto shouldTryUnbrandedToBrandedMigration = migration.shouldTryUnbrandedToBrandedMigration(); qCDebug(lcAccountState) << "shouldTryUnbrandedToBrandedMigration?" << shouldTryUnbrandedToBrandedMigration; qCDebug(lcAccountState) << "migrationPhase?" << migration.migrationPhase(); @@ -500,7 +500,7 @@ void AccountState::slotCredentialsFetched(AbstractCredentials *) << "attempting to connect"; _waitingForNewCredentials = false; ConfigFile configFile; - auto migration = configFile.migration(); + Migration migration; if (migration.isInProgress()) { migration.setMigrationPhase(Migration::MigrationPhase::Done); } diff --git a/src/gui/application.cpp b/src/gui/application.cpp index 4f6ccec3134c5..5b353f4a6af6f 100644 --- a/src/gui/application.cpp +++ b/src/gui/application.cpp @@ -166,7 +166,7 @@ bool Application::configVersionMigration() "Continuing will mean %2 these settings.
" "
" "The current configuration file was already backed up to %3.") - .arg((configFile.migration().isDowngrade() ? tr("newer", "newer software version") : tr("older", "older software version")), + .arg((migration.isDowngrade() ? tr("newer", "newer software version") : tr("older", "older software version")), deleteKeys.isEmpty()? tr("ignoring") : tr("deleting"), backupFilesList.join("
"))); box.addButton(tr("Quit"), QMessageBox::AcceptRole); @@ -479,7 +479,7 @@ void Application::setupAccountsAndFolders() { _folderManager.reset(new FolderMan); ConfigFile configFile; - auto migration = configFile.migration(); + Migration migration; migration.setMigrationPhase(Migration::MigrationPhase::SetupUsers); const auto accountsRestoreResult = restoreLegacyAccount(); if (accountsRestoreResult == AccountManager::AccountsNotFound || accountsRestoreResult == AccountManager::AccountsRestoreFailure) { diff --git a/src/gui/folderman.cpp b/src/gui/folderman.cpp index 9967368762bb2..f85f30f4032e7 100644 --- a/src/gui/folderman.cpp +++ b/src/gui/folderman.cpp @@ -352,7 +352,8 @@ int FolderMan::setupFoldersMigration() auto configPath = _folderConfigPath; #if !DISABLE_ACCOUNT_MIGRATION - if (const auto legacyConfigPath = cfg.migration().discoveredLegacyConfigPath(); !legacyConfigPath.isEmpty()) { + Migration migration; + if (const auto legacyConfigPath = migration.discoveredLegacyConfigPath(); !legacyConfigPath.isEmpty()) { configPath = legacyConfigPath; qCInfo(lcFolderMan) << "Starting folder migration from legacy path:" << legacyConfigPath; } diff --git a/src/libsync/configfile.cpp b/src/libsync/configfile.cpp index bab933fcbedf2..2da00dc560a9a 100644 --- a/src/libsync/configfile.cpp +++ b/src/libsync/configfile.cpp @@ -92,7 +92,6 @@ namespace chrono = std::chrono; Q_LOGGING_CATEGORY(lcConfigFile, "nextcloud.sync.configfile", QtInfoMsg) QString ConfigFile::_confDir = {}; -Migration ConfigFile::_migration = Migration{}; static chrono::milliseconds millisecondsValue(const QSettings &setting, const char *key, chrono::milliseconds defaultValue) @@ -327,7 +326,8 @@ void ConfigFile::restoreGeometryHeader(QHeaderView *header) QVariant ConfigFile::getPolicySetting(const QString &setting, const QVariant &defaultValue) const { if (Utility::isWindows()) { - const auto appName = migration().isUnbrandedToBrandedMigration() ? unbrandedAppName : Theme::instance()->appNameGUI(); + Migration migration; + const auto appName = migration.isUnbrandedToBrandedMigration() ? unbrandedAppName : Theme::instance()->appNameGUI(); // check for policies first and return immediately if a value is found. QSettings userPolicy(QString::fromLatin1(R"(HKEY_CURRENT_USER\Software\Policies\%1\%2)").arg(APPLICATION_VENDOR, appName), QSettings::NativeFormat); @@ -381,7 +381,10 @@ QString ConfigFile::excludeFile(Scope scope) const return ConfigFile::excludeFileFromSystem(); } - const auto excludeFilePath = scope == LegacyScope ? _migration.discoveredLegacyConfigPath() : configPath(); + Migration migration; + const auto excludeFilePath = scope == LegacyScope + ? migration.discoveredLegacyConfigPath() + : configPath(); // prefer sync-exclude.lst, but if it does not exist, check for exclude.lst QFileInfo exclFileInfo(excludeFilePath, syncExclFile); @@ -822,7 +825,8 @@ QVariant ConfigFile::getValue(const QString ¶m, const QString &group, const QVariant &defaultValue) const { QVariant systemSetting; - const auto appName = migration().isUnbrandedToBrandedMigration() ? unbrandedAppName : Theme::instance()->appNameGUI(); + Migration migration; + const auto appName = migration.isUnbrandedToBrandedMigration() ? unbrandedAppName : Theme::instance()->appNameGUI(); if (Utility::isMac()) { QSettings systemSettings(QLatin1String("/Library/Preferences/" APPLICATION_REV_DOMAIN ".plist"), QSettings::NativeFormat); if (!group.isEmpty()) { @@ -1350,8 +1354,4 @@ QStringList ConfigFile::backupConfigFiles() const return backupFilesList; } -Migration &ConfigFile::migration() { - return _migration; -} - } diff --git a/src/libsync/configfile.h b/src/libsync/configfile.h index 65894a9eadcae..ce981cd4aa73b 100644 --- a/src/libsync/configfile.h +++ b/src/libsync/configfile.h @@ -265,7 +265,6 @@ class OWNCLOUDSYNC_EXPORT ConfigFile [[nodiscard]] bool fileProviderDomainsAppSandboxMigrationCompleted() const; void setFileProviderDomainsAppSandboxMigrationCompleted(bool completed); - [[nodiscard]] static Migration &migration(); [[nodiscard]] QStringList backupConfigFiles() const; static constexpr char unbrandedAppName[] = "Nextcloud"; @@ -312,7 +311,6 @@ class OWNCLOUDSYNC_EXPORT ConfigFile using SharedCreds = QSharedPointer; static QString _confDir; - static Migration _migration; }; } #endif // CONFIGFILE_H diff --git a/src/libsync/settings/migration.cpp b/src/libsync/settings/migration.cpp index 0f4b8ac2b3e4e..24747e440a036 100644 --- a/src/libsync/settings/migration.cpp +++ b/src/libsync/settings/migration.cpp @@ -27,13 +27,11 @@ namespace OCC { Q_LOGGING_CATEGORY(lcMigration, "nextcloud.settings.migration", QtInfoMsg) -Migration::Migration() -{ - _migrationPhase = MigrationPhase::NotStarted; - _migrationType = MigrationType::UnbrandedToUnbranded; - _versionChangeType = VersionChangeType::NoVersionChange; - _discoveredLegacyConfigPath = {}; -} +Migration::MigrationPhase Migration::_migrationPhase = MigrationPhase::NotStarted;; +Migration::MigrationType Migration::_migrationType = MigrationType::UnbrandedToUnbranded; +Migration::VersionChangeType Migration::_versionChangeType = VersionChangeType::NoVersionChange; +QString Migration::_discoveredLegacyConfigPath = {}; +Migration::LegacyData Migration::_configSettings = {}; QVersionNumber Migration::currentVersion() const { @@ -195,12 +193,11 @@ Migration::LegacyData Migration::legacyData() const configFile.setClientPreviousVersionString(legacyVersion); qCInfo(lcMigration) << "Migrating from" << legacyVersion; qCInfo(lcMigration) << "Copy settings" << oCSettings->allKeys().join(", "); - Migration().setDiscoveredLegacyConfigPath(configFileInfo.canonicalPath()); - - + Migration migration; + migration.setDiscoveredLegacyConfigPath(configFileInfo.canonicalPath()); legacyData.configFile = configFileString; legacyData.settings.create(oCSettings.release()); - + migration.setLegacyData(legacyData); break; } else { qCInfo(lcMigration) << "Migrate: could not read old config " << configFileString; @@ -225,4 +222,10 @@ void Migration::setDiscoveredLegacyConfigPath(const QString &discoveredLegacyCon _discoveredLegacyConfigPath = discoveredLegacyConfigPath; } +void Migration::setLegacyData(const LegacyData &LegacyData) +{ + _configSettings.configFile = LegacyData.configFile; + _configSettings.settings = LegacyData.settings; +} + } diff --git a/src/libsync/settings/migration.h b/src/libsync/settings/migration.h index 967486947dd4b..868c78e5301c9 100644 --- a/src/libsync/settings/migration.h +++ b/src/libsync/settings/migration.h @@ -16,7 +16,7 @@ namespace OCC { class OWNCLOUDSYNC_EXPORT Migration { public: - Migration(); + Migration() { }; enum MigrationPhase { NotStarted, @@ -58,6 +58,7 @@ class OWNCLOUDSYNC_EXPORT Migration void setVersionChangeType(const VersionChangeType type); [[nodiscard]] LegacyData legacyData() const; + void setLegacyData(const LegacyData &legacyData); /// Set during first time migration of legacy accounts in AccountManager [[nodiscard]] QString discoveredLegacyConfigPath() const; @@ -73,11 +74,11 @@ class OWNCLOUDSYNC_EXPORT Migration [[nodiscard]] bool isInProgress() const; private: - MigrationPhase _migrationPhase; - MigrationType _migrationType; - VersionChangeType _versionChangeType; - QString _discoveredLegacyConfigPath; - LegacyData _configSettings; + static MigrationPhase _migrationPhase; + static MigrationType _migrationType; + static VersionChangeType _versionChangeType; + static QString _discoveredLegacyConfigPath; + static LegacyData _configSettings; }; } #endif // MIGRATION_H diff --git a/test/testmigration.cpp b/test/testmigration.cpp index ade95db4ea7ec..6e4ff799040ff 100644 --- a/test/testmigration.cpp +++ b/test/testmigration.cpp @@ -17,6 +17,7 @@ #include "syncenginetestutils.h" #include "testhelper.h" #include "version.h" +#include "settings/migration.h" using namespace OCC; @@ -159,7 +160,8 @@ private slots: // create Nextcloud config with older version setupStandarConfig("1.0.0"); const auto oldAppVersionNumber = QVersionNumber::fromString(_configFile.clientVersionString()); - QVERIFY(_configFile.isUpgrade()); + Migration migration; + QVERIFY(migration.isUpgrade()); // backup old config const auto backupFilesList = _configFile.backupConfigFiles(); @@ -200,8 +202,9 @@ private slots: QVERIFY(!configFile.exists()); // owncloud config files exists - configFile.findLegacyClientConfigFile(); - const auto legacyConfigFile = configFile.discoveredLegacyConfigFile(); + Migration migration; + const auto legacyData = migration.legacyData(); + const auto legacyConfigFile = legacyData.configFile; QVERIFY(!legacyConfigFile.isEmpty()); QCOMPARE(legacyConfigFile, ocConfig); @@ -250,8 +253,9 @@ private slots: QVERIFY(!configFile.exists()); // branded owncloud config files exists - configFile.findLegacyClientConfigFile(); - const auto legacyConfigFile = configFile.discoveredLegacyConfigFile(); + Migration migration; + const auto legacyData = migration.legacyData(); + const auto legacyConfigFile = legacyData.configFile; QVERIFY(!legacyConfigFile.isEmpty()); QCOMPARE(legacyConfigFile, ocBrandedConfig); } @@ -277,8 +281,9 @@ private slots: QVERIFY(!configFile.exists()); // owncloud config files exists - configFile.findLegacyClientConfigFile(); - const auto legacyConfigFile = configFile.discoveredLegacyConfigFile(); + Migration migration; + const auto legacyData = migration.legacyData(); + const auto legacyConfigFile = legacyData.configFile; QVERIFY(!legacyConfigFile.isEmpty()); QCOMPARE(legacyConfigFile, brandedConfig); } From 2a21520c4256d78bcf6004a027dc508848f39c47 Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Thu, 8 Jan 2026 18:33:15 +0100 Subject: [PATCH 9/9] refactor: rename Migration class members. Signed-off-by: Camila Ayres --- src/gui/accountmanager.cpp | 9 ++-- src/gui/accountstate.cpp | 4 +- src/gui/application.cpp | 10 ++--- src/libsync/settings/migration.cpp | 66 +++++++++++++++--------------- src/libsync/settings/migration.h | 37 ++++++++--------- test/testmigration.cpp | 12 ++---- 6 files changed, 63 insertions(+), 75 deletions(-) diff --git a/src/gui/accountmanager.cpp b/src/gui/accountmanager.cpp index ab7c7cc03161a..689024bf71dab 100644 --- a/src/gui/accountmanager.cpp +++ b/src/gui/accountmanager.cpp @@ -201,19 +201,18 @@ bool AccountManager::restoreFromLegacySettings() auto wasLegacyImportDialogDisplayed = false; QStringList selectedAccountIds; Migration migration; - if (const auto legacyData = migration.legacyData(); !legacyData.configFile.isEmpty()) { + if (const auto legacyData = migration.legacyData(); !legacyData.isNull()) { const auto displayLegacyImportDialog = Theme::instance()->displayLegacyImportDialog(); - const auto configFile = legacyData.configFile; - auto oCSettings = std::move(legacyData.settings); + auto oCSettings = std::move(legacyData); oCSettings->beginGroup(QLatin1String(accountsC)); const auto childGroups = oCSettings->childGroups(); const auto accountsListSize = childGroups.size(); oCSettings->endGroup(); // accountsC - qCInfo(lcAccountManager) << "Migrate: checking old config " << configFile; + qCInfo(lcAccountManager) << "Migrate: checking old config"; if (!forceLegacyImport() && displayLegacyImportDialog && accountsListSize > 0) { wasLegacyImportDialogDisplayed = true; if (childGroups.size() == 1) { @@ -300,7 +299,7 @@ bool AccountManager::restoreFromLegacySettings() configFile.setDownloadLimit(settings->value(ConfigFile::downloadLimitC, configFile.downloadLimit()).toInt()); // Try to load the single account. - migration.setMigrationPhase(Migration::MigrationPhase::SetupUsers); + migration.setPhase(Migration::Phase::SetupUsers); if (!settings->childKeys().isEmpty()) { settings->beginGroup(accountsC); const auto childGroups = selectedAccountIds.isEmpty() ? settings->childGroups() : selectedAccountIds; diff --git a/src/gui/accountstate.cpp b/src/gui/accountstate.cpp index f1a4f88b3f5d7..78fea62ebd0e1 100644 --- a/src/gui/accountstate.cpp +++ b/src/gui/accountstate.cpp @@ -301,7 +301,7 @@ void AccountState::checkConnectivity() Migration migration; const auto shouldTryUnbrandedToBrandedMigration = migration.shouldTryUnbrandedToBrandedMigration(); qCDebug(lcAccountState) << "shouldTryUnbrandedToBrandedMigration?" << shouldTryUnbrandedToBrandedMigration; - qCDebug(lcAccountState) << "migrationPhase?" << migration.migrationPhase(); + qCDebug(lcAccountState) << "migration Phase?" << migration.phase(); const auto appName = shouldTryUnbrandedToBrandedMigration ? configFile.unbrandedAppName : ""; account()->credentials()->fetchFromKeychain(appName); return; @@ -502,7 +502,7 @@ void AccountState::slotCredentialsFetched(AbstractCredentials *) ConfigFile configFile; Migration migration; if (migration.isInProgress()) { - migration.setMigrationPhase(Migration::MigrationPhase::Done); + migration.setPhase(Migration::Phase::Done); } checkConnectivity(); } diff --git a/src/gui/application.cpp b/src/gui/application.cpp index 5b353f4a6af6f..e703ba443e6d1 100644 --- a/src/gui/application.cpp +++ b/src/gui/application.cpp @@ -126,11 +126,11 @@ bool Application::configVersionMigration() const auto shouldTryToMigrate = migration.shouldTryToMigrate(); if (!shouldTryToMigrate) { qCInfo(lcApplication) << "This is not an upgrade/downgrade/migration. Proceed to read current application config file."; - migration.setMigrationPhase(Migration::MigrationPhase::Done); + migration.setPhase(Migration::Phase::Done); return false; } - migration.setMigrationPhase(Migration::MigrationPhase::SetupConfigFile); + migration.setPhase(Migration::Phase::SetupConfigFile); QStringList deleteKeys, ignoreKeys; AccountManager::backwardMigrationSettingsKeys(&deleteKeys, &ignoreKeys); FolderMan::backwardMigrationSettingsKeys(&deleteKeys, &ignoreKeys); @@ -480,17 +480,17 @@ void Application::setupAccountsAndFolders() _folderManager.reset(new FolderMan); ConfigFile configFile; Migration migration; - migration.setMigrationPhase(Migration::MigrationPhase::SetupUsers); + migration.setPhase(Migration::Phase::SetupUsers); const auto accountsRestoreResult = restoreLegacyAccount(); if (accountsRestoreResult == AccountManager::AccountsNotFound || accountsRestoreResult == AccountManager::AccountsRestoreFailure) { qCWarning(lcApplication) << "Migration result: " << accountsRestoreResult; qCDebug(lcApplication) << "is migration disabled?" << DISABLE_ACCOUNT_MIGRATION; qCWarning(lcApplication) << "No accounts were migrated, prompting user to set up accounts and folders from scratch."; - migration.setMigrationPhase(Migration::MigrationPhase::Done); + migration.setPhase(Migration::Phase::Done); return; } - migration.setMigrationPhase(Migration::MigrationPhase::SetupFolders); + migration.setPhase(Migration::Phase::SetupFolders); const auto foldersListSize = FolderMan::instance()->setupFolders(); FolderMan::instance()->setSyncEnabled(true); diff --git a/src/libsync/settings/migration.cpp b/src/libsync/settings/migration.cpp index 24747e440a036..7ca9846d4c703 100644 --- a/src/libsync/settings/migration.cpp +++ b/src/libsync/settings/migration.cpp @@ -27,11 +27,11 @@ namespace OCC { Q_LOGGING_CATEGORY(lcMigration, "nextcloud.settings.migration", QtInfoMsg) -Migration::MigrationPhase Migration::_migrationPhase = MigrationPhase::NotStarted;; -Migration::MigrationType Migration::_migrationType = MigrationType::UnbrandedToUnbranded; -Migration::VersionChangeType Migration::_versionChangeType = VersionChangeType::NoVersionChange; +Migration::Phase Migration::_phase = Phase::NotStarted;; +Migration::BrandingType Migration::_brandingType = BrandingType::UnbrandedToUnbranded; +Migration::UpgradeType Migration::_upgradeType = UpgradeType::NoChange; QString Migration::_discoveredLegacyConfigPath = {}; -Migration::LegacyData Migration::_configSettings = {}; +Migration::LegacyData Migration::_legacyData = {}; QVersionNumber Migration::currentVersion() const { @@ -48,55 +48,55 @@ QVersionNumber Migration::configVersion() const return QVersionNumber::fromString(ConfigFile().clientVersionString()); } -void Migration::setMigrationPhase(const MigrationPhase phase) +void Migration::setPhase(const Phase phase) { // do not rollback - if (phase > _migrationPhase) { - _migrationPhase = phase; + if (phase > _phase) { + _phase = phase; } } -Migration::MigrationPhase Migration::migrationPhase() const +Migration::Phase Migration::phase() const { - return _migrationPhase; + return _phase; } -void Migration::setMigrationType(const MigrationType type) +void Migration::setBrandingType(const BrandingType type) { - _migrationType = type; + _brandingType = type; } -Migration::MigrationType Migration::migrationType() const +Migration::BrandingType Migration::brandingType() const { - return _migrationType; + return _brandingType; } -Migration::VersionChangeType Migration::versionChangeType() const +Migration::UpgradeType Migration::upgradeType() const { - return _versionChangeType; + return _upgradeType; } -void Migration::setVersionChangeType(const VersionChangeType type) +void Migration::setUpgradeType(const UpgradeType type) { - _versionChangeType = type; + _upgradeType = type; } bool Migration::isUpgrade() { const auto isUpgrade = currentVersion() > previousVersion(); if (isUpgrade) { - setVersionChangeType(VersionChangeType::Upgrade); + setUpgradeType(UpgradeType::Upgrade); } - return versionChangeType() == VersionChangeType::Upgrade; + return _upgradeType == UpgradeType::Upgrade; } bool Migration::isDowngrade() { const auto isDowngrade = previousVersion() > currentVersion(); if (isDowngrade) { - setVersionChangeType(VersionChangeType::Downgrade); + setUpgradeType(UpgradeType::Downgrade); } - return versionChangeType() == VersionChangeType::Downgrade; + return _upgradeType == UpgradeType::Downgrade; } bool Migration::versionChanged() @@ -104,21 +104,21 @@ bool Migration::versionChanged() return isUpgrade() || isDowngrade(); } -bool Migration::shouldTryUnbrandedToBrandedMigration() const +bool Migration::shouldTryUnbrandedToBrandedMigration() { - const auto isUnbrandedToBranded = migrationPhase() == Migration::MigrationPhase::SetupFolders + const auto isUnbrandedToBranded = phase() == Migration::Phase::SetupFolders && Theme::instance()->appName() != ConfigFile::unbrandedAppName && !_discoveredLegacyConfigPath.isEmpty(); if (isUnbrandedToBranded) { - Migration().setMigrationType(MigrationType::UnbrandedToBranded); + setBrandingType(BrandingType::UnbrandedToBranded); } - return migrationType() == MigrationType::UnbrandedToBranded; + return _brandingType == BrandingType::UnbrandedToBranded; } bool Migration::isUnbrandedToBrandedMigration() const { - return isInProgress() && migrationType() == MigrationType::UnbrandedToBranded; + return isInProgress() && brandingType() == BrandingType::UnbrandedToBranded; } bool Migration::shouldTryToMigrate() @@ -136,9 +136,9 @@ bool Migration::isClientVersionSet() const bool Migration::isInProgress() const { - const auto currentPhase = migrationPhase(); - return currentPhase != MigrationPhase::NotStarted - && currentPhase != MigrationPhase::Done; + const auto currentPhase = phase(); + return currentPhase != Phase::NotStarted + && currentPhase != Phase::Done; } Migration::LegacyData Migration::legacyData() const @@ -195,8 +195,7 @@ Migration::LegacyData Migration::legacyData() const qCInfo(lcMigration) << "Copy settings" << oCSettings->allKeys().join(", "); Migration migration; migration.setDiscoveredLegacyConfigPath(configFileInfo.canonicalPath()); - legacyData.configFile = configFileString; - legacyData.settings.create(oCSettings.release()); + legacyData.reset(oCSettings.get()); migration.setLegacyData(legacyData); break; } else { @@ -222,10 +221,9 @@ void Migration::setDiscoveredLegacyConfigPath(const QString &discoveredLegacyCon _discoveredLegacyConfigPath = discoveredLegacyConfigPath; } -void Migration::setLegacyData(const LegacyData &LegacyData) +void Migration::setLegacyData(const LegacyData legacyData) { - _configSettings.configFile = LegacyData.configFile; - _configSettings.settings = LegacyData.settings; + _legacyData = legacyData; } } diff --git a/src/libsync/settings/migration.h b/src/libsync/settings/migration.h index 868c78e5301c9..a5993ef2395a1 100644 --- a/src/libsync/settings/migration.h +++ b/src/libsync/settings/migration.h @@ -18,7 +18,7 @@ class OWNCLOUDSYNC_EXPORT Migration public: Migration() { }; - enum MigrationPhase { + enum Phase { NotStarted, SetupConfigFile, SetupUsers, @@ -26,39 +26,36 @@ class OWNCLOUDSYNC_EXPORT Migration Done }; - enum MigrationType { + enum BrandingType { UnbrandedToUnbranded, UnbrandedToBranded, LegacyToUnbranded, LegacyToBranded }; - enum VersionChangeType { - NoVersionChange, + enum UpgradeType { + NoChange, Upgrade, Downgrade }; - struct LegacyData { - QString configFile; - QSharedPointer settings; - }; + using LegacyData = QSharedPointer; [[nodiscard]] QVersionNumber previousVersion() const; [[nodiscard]] QVersionNumber currentVersion() const; [[nodiscard]] QVersionNumber configVersion() const; - [[nodiscard]] MigrationPhase migrationPhase() const; - void setMigrationPhase(const MigrationPhase phase); + [[nodiscard]] Phase phase() const; + void setPhase(const Phase phase); - [[nodiscard]] MigrationType migrationType() const; - void setMigrationType(const MigrationType type); + [[nodiscard]] BrandingType brandingType() const; + void setBrandingType(const BrandingType type); - [[nodiscard]] VersionChangeType versionChangeType() const; - void setVersionChangeType(const VersionChangeType type); + [[nodiscard]] UpgradeType upgradeType() const; + void setUpgradeType(const UpgradeType type); [[nodiscard]] LegacyData legacyData() const; - void setLegacyData(const LegacyData &legacyData); + void setLegacyData(const LegacyData legacyData); /// Set during first time migration of legacy accounts in AccountManager [[nodiscard]] QString discoveredLegacyConfigPath() const; @@ -67,18 +64,18 @@ class OWNCLOUDSYNC_EXPORT Migration [[nodiscard]] bool isUpgrade(); [[nodiscard]] bool isDowngrade(); [[nodiscard]] bool versionChanged(); - [[nodiscard]] bool shouldTryUnbrandedToBrandedMigration() const; + [[nodiscard]] bool shouldTryUnbrandedToBrandedMigration(); [[nodiscard]] bool isUnbrandedToBrandedMigration() const; [[nodiscard]] bool shouldTryToMigrate(); [[nodiscard]] bool isClientVersionSet() const; [[nodiscard]] bool isInProgress() const; private: - static MigrationPhase _migrationPhase; - static MigrationType _migrationType; - static VersionChangeType _versionChangeType; + static Phase _phase; + static BrandingType _brandingType; + static UpgradeType _upgradeType; static QString _discoveredLegacyConfigPath; - static LegacyData _configSettings; + static LegacyData _legacyData; }; } #endif // MIGRATION_H diff --git a/test/testmigration.cpp b/test/testmigration.cpp index 6e4ff799040ff..d56957faca4dc 100644 --- a/test/testmigration.cpp +++ b/test/testmigration.cpp @@ -204,9 +204,7 @@ private slots: // owncloud config files exists Migration migration; const auto legacyData = migration.legacyData(); - const auto legacyConfigFile = legacyData.configFile; - QVERIFY(!legacyConfigFile.isEmpty()); - QCOMPARE(legacyConfigFile, ocConfig); + QVERIFY(!legacyData.isNull()); // TODO: add accounts and folders to AccountManager and FolderMan without UI interference //_folderMan.reset({}); @@ -255,9 +253,7 @@ private slots: // branded owncloud config files exists Migration migration; const auto legacyData = migration.legacyData(); - const auto legacyConfigFile = legacyData.configFile; - QVERIFY(!legacyConfigFile.isEmpty()); - QCOMPARE(legacyConfigFile, ocBrandedConfig); + QVERIFY(!legacyData.isNull()); } @@ -283,9 +279,7 @@ private slots: // owncloud config files exists Migration migration; const auto legacyData = migration.legacyData(); - const auto legacyConfigFile = legacyData.configFile; - QVERIFY(!legacyConfigFile.isEmpty()); - QCOMPARE(legacyConfigFile, brandedConfig); + QVERIFY(!legacyData.isNull()); } // TODO: Downgrade