From 165568ee1c33784296a7068b3033894fd1c07dd1 Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Thu, 4 Dec 2025 22:02:20 +0100 Subject: [PATCH 1/8] refactor(migration): 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/gui/accountmanager.cpp | 7 +- src/gui/accountstate.cpp | 11 ++- src/gui/application.cpp | 25 +++--- src/libsync/CMakeLists.txt | 2 + src/libsync/configfile.cpp | 63 ++------------ src/libsync/configfile.h | 22 +---- src/libsync/settings/migration.cpp | 130 +++++++++++++++++++++++++++++ src/libsync/settings/migration.h | 67 +++++++++++++++ 8 files changed, 233 insertions(+), 94 deletions(-) create mode 100644 src/libsync/settings/migration.cpp create mode 100644 src/libsync/settings/migration.h diff --git a/src/gui/accountmanager.cpp b/src/gui/accountmanager.cpp index 7bbf7fecce77d..58e0946ec6500 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; @@ -694,8 +696,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 db3ad8a3ec064..5045d9e02f7e6 100644 --- a/src/gui/application.cpp +++ b/src/gui/application.cpp @@ -33,6 +33,7 @@ #include "common/vfs.h" #include "config.h" +#include "settings/migration.h" #if defined(Q_OS_WIN) #include @@ -121,21 +122,20 @@ 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); - configFile.setClientPreviousVersionString(configFile.clientVersionString()); - - 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(); @@ -181,7 +181,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 +493,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 +519,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/CMakeLists.txt b/src/libsync/CMakeLists.txt index 10429efe187cb..df20438585914 100644 --- a/src/libsync/CMakeLists.txt +++ b/src/libsync/CMakeLists.txt @@ -166,6 +166,8 @@ set(libsync_SRCS creds/keychainchunk.cpp caseclashconflictsolver.h caseclashconflictsolver.cpp + settings/migration.h + settings/migration.cpp ) if (WIN32) 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 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 4c45852c349e568fed8ca889545f34fdfdf47f11 Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Wed, 30 Apr 2025 22:12:05 +0200 Subject: [PATCH 2/8] test(migration): basic class tests. Signed-off-by: Camila Ayres --- src/gui/folderman.h | 2 + src/libsync/settings/migration.cpp | 12 ++ test/CMakeLists.txt | 1 + test/testmigration.cpp | 220 +++++++++++++++++++++++++++++ 4 files changed, 235 insertions(+) create mode 100644 test/testmigration.cpp diff --git a/src/gui/folderman.h b/src/gui/folderman.h index 0b91633371010..e6dad3b342268 100644 --- a/src/gui/folderman.h +++ b/src/gui/folderman.h @@ -29,6 +29,7 @@ class TestSyncConflictsModel; class TestRemoteWipe; class FolderManTestHelper; class TestFileActionsModel; +class TestMigration; namespace OCC { @@ -420,6 +421,7 @@ private slots: friend class ::TestRemoteWipe; friend class ::FolderManTestHelper; friend class ::TestFileActionsModel; + friend class ::TestMigration; }; } // namespace OCC diff --git a/src/libsync/settings/migration.cpp b/src/libsync/settings/migration.cpp index 0f175b6997bb1..c4e1f6daa54c5 100644 --- a/src/libsync/settings/migration.cpp +++ b/src/libsync/settings/migration.cpp @@ -9,6 +9,18 @@ #include "theme.h" #include "configfile.h" #include "version.h" +#include "common/utility.h" + +#include +#include +#include + +namespace { + constexpr auto legacyCfgFileNameC = "owncloud.cfg"; + constexpr auto legacyRelativeConfigLocationC = "/ownCloud/owncloud.cfg"; + constexpr auto unbrandedRelativeConfigLocationC = "/Nextcloud/nextcloud.cfg"; + constexpr auto unbrandedCfgFileNameC = "nextcloud.cfg"; +} namespace OCC { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 22702af53a303..96689bac4a939 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -154,6 +154,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..e3fb2c563d584 --- /dev/null +++ b/test/testmigration.cpp @@ -0,0 +1,220 @@ +/* + * 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"); + // TODO: detangle UI from logic + //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); + } + + void testSetPhase() + { + Migration migration; + QCOMPARE(migration.phase(), OCC::Migration::Phase::NotStarted); + migration.setPhase(OCC::Migration::Phase::SetupConfigFile); + QCOMPARE(migration.phase(), OCC::Migration::Phase::SetupConfigFile); + migration.setPhase(OCC::Migration::Phase::SetupUsers); + QCOMPARE(migration.phase(), OCC::Migration::Phase::SetupUsers); + migration.setPhase(OCC::Migration::Phase::SetupFolders); + QCOMPARE(migration.phase(), OCC::Migration::Phase::SetupFolders); + migration.setPhase(OCC::Migration::Phase::Done); + QCOMPARE(migration.phase(), OCC::Migration::Phase::Done); + } + + void testSetUpgradeType() + { + Migration migration; + QCOMPARE(migration.upgradeType(), OCC::Migration::UpgradeType::NoChange); + migration.setUpgradeType(OCC::Migration::UpgradeType::Upgrade); + QCOMPARE(migration.upgradeType(), OCC::Migration::UpgradeType::Upgrade); + migration.setUpgradeType(OCC::Migration::UpgradeType::Downgrade); + QCOMPARE(migration.upgradeType(), OCC::Migration::UpgradeType::Downgrade); + } + + void testSetBrandingType() + { + Migration migration; + QCOMPARE(migration.brandingType(), OCC::Migration::BrandingType::UnbrandedToUnbranded); + migration.setBrandingType(OCC::Migration::BrandingType::LegacyToUnbranded); + QCOMPARE(migration.brandingType(), OCC::Migration::BrandingType::LegacyToUnbranded); + migration.setBrandingType(OCC::Migration::BrandingType::LegacyToBranded); + QCOMPARE(migration.brandingType(), OCC::Migration::BrandingType::LegacyToBranded); + migration.setBrandingType(OCC::Migration::BrandingType::UnbrandedToBranded); + QCOMPARE(migration.brandingType(), OCC::Migration::BrandingType::UnbrandedToBranded); + } + + void testSetDiscoveredLegacyConfigPath() + { + Migration migration; + QCOMPARE(migration.discoveredLegacyConfigPath(), QString()); + const auto legacyConfigPath = QString("/path/to/legacy/config"); + migration.setDiscoveredLegacyConfigPath(legacyConfigPath); + QCOMPARE(migration.discoveredLegacyConfigPath(), legacyConfigPath); + } + + void testUpgrade() + { + // create Nextcloud config with older version + setupStandarConfig("1.0.0"); + Migration migration; + QCOMPARE(migration.isUpgrade(), true); + + // 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); + QCOMPARE(_configFile.clientVersionString(), MIRALL_VERSION_STRING); + } +}; + +QTEST_GUILESS_MAIN(TestMigration) +#include "testmigration.moc" From 99d2a8703a19529f6ef461de576c00a1cbb66444 Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Wed, 7 Jan 2026 10:48:33 +0100 Subject: [PATCH 3/8] fix(migration): 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 c4e1f6daa54c5..b476940c54960 100644 --- a/src/libsync/settings/migration.cpp +++ b/src/libsync/settings/migration.cpp @@ -107,7 +107,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 489ace44f01f9a4d3190ce99ce6f3c6153e29d4e Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Thu, 8 Jan 2026 13:08:34 +0100 Subject: [PATCH 4/8] refactor(migration): 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 5045d9e02f7e6..2bf8d994e00a6 100644 --- a/src/gui/application.cpp +++ b/src/gui/application.cpp @@ -153,25 +153,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, @@ -181,7 +165,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 66af508e789bade4f32cd6fd063148332965edcb Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Thu, 8 Jan 2026 13:11:04 +0100 Subject: [PATCH 5/8] refactor(migration): find and readlegacy config files in the Migration class. - Make all Migration class members static. - Rename Migration class members. Signed-off-by: Camila Ayres --- src/gui/accountmanager.cpp | 152 ++++++++++------------------- src/gui/accountstate.cpp | 8 +- src/gui/application.cpp | 34 +++++-- src/gui/folderman.cpp | 4 +- src/gui/folderman.h | 1 + src/libsync/configfile.cpp | 16 +-- src/libsync/configfile.h | 2 - src/libsync/settings/migration.cpp | 148 ++++++++++++++++++++++------ src/libsync/settings/migration.h | 44 ++++++--- test/testmigration.cpp | 1 + 10 files changed, 239 insertions(+), 171 deletions(-) diff --git a/src/gui/accountmanager.cpp b/src/gui/accountmanager.cpp index 58e0946ec6500..bc3983634112b 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; + Migration migration; + if (const auto legacyData = migration.legacyData(); !legacyData.isNull()) { + + const auto displayLegacyImportDialog = Theme::instance()->displayLegacyImportDialog(); + + 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"; + 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; @@ -346,8 +299,7 @@ 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); + migration.setPhase(Migration::Phase::SetupUsers); if (!settings->childKeys().isEmpty()) { settings->beginGroup(accountsC); const auto childGroups = selectedAccountIds.isEmpty() ? settings->childGroups() : selectedAccountIds; @@ -696,7 +648,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..78fea62ebd0e1 100644 --- a/src/gui/accountstate.cpp +++ b/src/gui/accountstate.cpp @@ -298,10 +298,10 @@ 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(); + qCDebug(lcAccountState) << "migration Phase?" << migration.phase(); const auto appName = shouldTryUnbrandedToBrandedMigration ? configFile.unbrandedAppName : ""; account()->credentials()->fetchFromKeychain(appName); return; @@ -500,9 +500,9 @@ 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); + migration.setPhase(Migration::Phase::Done); } checkConnectivity(); } diff --git a/src/gui/application.cpp b/src/gui/application.cpp index 2bf8d994e00a6..0603f8914f5b8 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); @@ -153,9 +153,25 @@ 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 and message the user either for destructive changes, + // 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, // or if we're ignoring something and the client version changed. - if (const auto backupFilesList = configFile.backupConfigFiles(); configFile.showConfigBackupWarning() && backupFilesList.count() > 0) { + if (configFile.showConfigBackupWarning() && backupFilesList.count() > 0) { QMessageBox box( QMessageBox::Warning, APPLICATION_SHORTNAME, @@ -165,7 +181,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); @@ -477,18 +493,18 @@ void Application::setupAccountsAndFolders() { _folderManager.reset(new FolderMan); ConfigFile configFile; - auto migration = configFile.migration(); - migration.setMigrationPhase(Migration::MigrationPhase::SetupUsers); + Migration migration; + 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/gui/folderman.cpp b/src/gui/folderman.cpp index ec68cf6435302..bd7d53d139f55 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 @@ -403,7 +404,8 @@ int FolderMan::setupFoldersMigration() auto configPath = _folderConfigPath; #if !DISABLE_ACCOUNT_MIGRATION - if (const auto legacyConfigPath = ConfigFile::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/gui/folderman.h b/src/gui/folderman.h index e6dad3b342268..b5c344a6c207e 100644 --- a/src/gui/folderman.h +++ b/src/gui/folderman.h @@ -38,6 +38,7 @@ class SyncResult; class SocketApi; class LockWatcher; class UpdateE2eeFolderUsersMetadataJob; +class Migration; /** * @brief The FolderMan class 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 b476940c54960..6627b2f0909e2 100644 --- a/src/libsync/settings/migration.cpp +++ b/src/libsync/settings/migration.cpp @@ -26,12 +26,11 @@ namespace OCC { Q_LOGGING_CATEGORY(lcMigration, "nextcloud.settings.migration", QtInfoMsg) -Migration::Migration() -{ - _migrationPhase = MigrationPhase::NotStarted; - _migrationType = MigrationType::UnbrandedToUnbranded; - _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::_legacyData = {}; QVersionNumber Migration::currentVersion() const { @@ -48,55 +47,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 +103,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 - && !ConfigFile().discoveredLegacyConfigPath().isEmpty(); + && !_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 +135,94 @@ 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 +{ + 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 migration; + migration.setDiscoveredLegacyConfigPath(configFileInfo.canonicalPath()); + legacyData.reset(oCSettings.get()); + migration.setLegacyData(legacyData); + 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; +} + +void Migration::setLegacyData(const LegacyData legacyData) +{ + _legacyData = legacyData; } } diff --git a/src/libsync/settings/migration.h b/src/libsync/settings/migration.h index d6f2f85c3e5f4..a5993ef2395a1 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 { @@ -14,9 +16,9 @@ namespace OCC { class OWNCLOUDSYNC_EXPORT Migration { public: - Migration(); + Migration() { }; - enum MigrationPhase { + enum Phase { NotStarted, SetupConfigFile, SetupUsers, @@ -24,44 +26,56 @@ class OWNCLOUDSYNC_EXPORT Migration Done }; - enum MigrationType { + enum BrandingType { UnbrandedToUnbranded, UnbrandedToBranded, LegacyToUnbranded, LegacyToBranded }; - enum VersionChangeType { - NoVersionChange, + enum UpgradeType { + NoChange, Upgrade, Downgrade }; + using LegacyData = QSharedPointer; + [[nodiscard]] QVersionNumber previousVersion() const; [[nodiscard]] QVersionNumber currentVersion() const; [[nodiscard]] QVersionNumber configVersion() const; - [[nodiscard]] MigrationPhase migrationPhase() const; - [[nodiscard]] MigrationType migrationType() const; - [[nodiscard]] VersionChangeType versionChangeType() const; + [[nodiscard]] Phase phase() const; + void setPhase(const Phase phase); + + [[nodiscard]] BrandingType brandingType() const; + void setBrandingType(const BrandingType type); + + [[nodiscard]] UpgradeType upgradeType() const; + void setUpgradeType(const UpgradeType type); - void setMigrationPhase(const MigrationPhase phase); - void setMigrationType(const MigrationType type); - 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; + void setDiscoveredLegacyConfigPath(const QString &discoveredLegacyConfigPath); + [[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: - MigrationPhase _migrationPhase; - MigrationType _migrationType; - VersionChangeType _versionChangeType; + static Phase _phase; + static BrandingType _brandingType; + static UpgradeType _upgradeType; + static QString _discoveredLegacyConfigPath; + static LegacyData _legacyData; }; } #endif // MIGRATION_H diff --git a/test/testmigration.cpp b/test/testmigration.cpp index e3fb2c563d584..371989ca803e6 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; From 0b0dc97c04a041b2d4f495282ce9021fca4ec4d2 Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Tue, 3 Mar 2026 18:28:54 +0100 Subject: [PATCH 6/8] refactor(migration): change log level, add comments. Signed-off-by: Camila Ayres --- src/libsync/settings/migration.cpp | 4 ++-- src/libsync/settings/migration.h | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libsync/settings/migration.cpp b/src/libsync/settings/migration.cpp index 6627b2f0909e2..988238db2d827 100644 --- a/src/libsync/settings/migration.cpp +++ b/src/libsync/settings/migration.cpp @@ -190,8 +190,8 @@ Migration::LegacyData Migration::legacyData() const 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(", "); + qCInfo(lcMigration) << "Migrating from legacy version" << legacyVersion; + qCDebug(lcMigration) << "Copy settings" << oCSettings->allKeys().join(", "); Migration migration; migration.setDiscoveredLegacyConfigPath(configFileInfo.canonicalPath()); legacyData.reset(oCSettings.get()); diff --git a/src/libsync/settings/migration.h b/src/libsync/settings/migration.h index a5993ef2395a1..1c7df6f015c60 100644 --- a/src/libsync/settings/migration.h +++ b/src/libsync/settings/migration.h @@ -54,6 +54,7 @@ class OWNCLOUDSYNC_EXPORT Migration [[nodiscard]] UpgradeType upgradeType() const; void setUpgradeType(const UpgradeType type); + /// Returns QSettings from a legacy config file [[nodiscard]] LegacyData legacyData() const; void setLegacyData(const LegacyData legacyData); From b1cf250ef85dacd4b404a254e895a490ca233ba9 Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Tue, 3 Mar 2026 22:26:28 +0100 Subject: [PATCH 7/8] chore(migration): add extended comment to explain the migration phases. Signed-off-by: Camila Ayres --- src/libsync/settings/migration.h | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/libsync/settings/migration.h b/src/libsync/settings/migration.h index 1c7df6f015c60..012bad1f727ac 100644 --- a/src/libsync/settings/migration.h +++ b/src/libsync/settings/migration.h @@ -45,6 +45,18 @@ class OWNCLOUDSYNC_EXPORT Migration [[nodiscard]] QVersionNumber currentVersion() const; [[nodiscard]] QVersionNumber configVersion() const; + /** + * => Application::configVersionMigration: check existing config + * => no upgrade/downgrade/migration: Migration::Phase::Done + * => need to set config file: Migration::Phase::SetupConfigFile + * => Application::setupAccountsAndFolders: Migration::Phase::SetupUsers + * => Application::restoreLegacyAccount + * => AccountManager::restore + * => AccountManager::restoreFromLegacySettings: + * => Migration::legacyData: find legacy config and read its settings using + * => FolderMan::setupFolders: Migration::Phase::SetupFolders + * => AccountState::slotCredentialsFetched: Migration::Phase::Done + */ [[nodiscard]] Phase phase() const; void setPhase(const Phase phase); From 2539f954b25ce30ccca063888886f4537b956c5c Mon Sep 17 00:00:00 2001 From: Camila Ayres Date: Tue, 3 Mar 2026 22:33:09 +0100 Subject: [PATCH 8/8] fix(migration): update function change from ConfigFile to Migration. Signed-off-by: Camila Ayres --- src/gui/accountmanager.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/gui/accountmanager.cpp b/src/gui/accountmanager.cpp index bc3983634112b..f2e62f00d27a4 100644 --- a/src/gui/accountmanager.cpp +++ b/src/gui/accountmanager.cpp @@ -499,8 +499,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; + Migration 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();