diff --git a/src/App/ApplicationDirectories.cpp b/src/App/ApplicationDirectories.cpp index 2102bd49bc..08685a29c5 100644 --- a/src/App/ApplicationDirectories.cpp +++ b/src/App/ApplicationDirectories.cpp @@ -41,6 +41,8 @@ #include #include +#include "Base/Console.h" + using namespace App; namespace fs = std::filesystem; @@ -567,6 +569,7 @@ void ApplicationDirectories::migrateAllPaths(const std::vector &paths) } else { newPath = path / versionStringForPath(major, minor); } + Base::Console().message("Migrating config from %s to %s\n", Base::FileInfo::pathToString(path), Base::FileInfo::pathToString(newPath)); if (fs::exists(newPath)) { throw Base::RuntimeError("Cannot migrate config - path already exists: " + Base::FileInfo::pathToString(newPath)); } diff --git a/src/Gui/CMakeLists.txt b/src/Gui/CMakeLists.txt index fbd4fe2f98..447056d4c5 100644 --- a/src/Gui/CMakeLists.txt +++ b/src/Gui/CMakeLists.txt @@ -1386,6 +1386,7 @@ SET(FreeCADGui_CPP_SRCS StartupProcess.cpp TransactionObject.cpp ToolHandler.cpp + VersionMigrator.cpp StyleParameters/Parser.cpp StyleParameters/ParameterManager.cpp ) @@ -1429,6 +1430,7 @@ SET(FreeCADGui_SRCS StartupProcess.h TransactionObject.h ToolHandler.h + VersionMigrator.h StyleParameters/Parser.h StyleParameters/ParameterManager.h ) diff --git a/src/Gui/StartupProcess.cpp b/src/Gui/StartupProcess.cpp index 820017cb29..5d00a0348b 100644 --- a/src/Gui/StartupProcess.cpp +++ b/src/Gui/StartupProcess.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -52,12 +53,13 @@ #include "GuiApplication.h" #include "MainWindow.h" #include "Language/Translator.h" +#include "VersionMigrator.h" #include -#include #include using namespace Gui; +namespace fs = std::filesystem; StartupProcess::StartupProcess() = default; @@ -231,7 +233,7 @@ void StartupPostProcess::execute() showMainWindow(); activateWorkbench(); checkParameters(); - runWelcomeScreen(); + checkVersionMigration(); } void StartupPostProcess::setWindowTitle() @@ -557,145 +559,7 @@ void StartupPostProcess::checkParameters() } } -void StartupPostProcess::runWelcomeScreen() -{ - // If the user is running a custom directory set, there is no migration to versioned directories - if (App::Application::directories()->usingCustomDirectories()) { - return; - } - - auto prefGroup = App::GetApplication().GetParameterGroupByPath( - "User parameter:BaseApp/Preferences/Migration"); - - // Split our comma-separated list of already-migrated-to version directories into a set for easy - // searching - auto splitCommas = [](const std::string &input) { - std::set result; - std::stringstream ss(input); - std::string token; - - while (std::getline(ss, token, ',')) { - result.insert(token); - } - - return result; - }; - - std::string offeredToMigrateToVersionedConfig = - prefGroup->GetASCII("OfferedToMigrateToVersionedConfig", ""); - std::set knownVersions; - if (!offeredToMigrateToVersionedConfig.empty()) { - knownVersions = splitCommas(offeredToMigrateToVersionedConfig); - } - - auto joinCommas = [](const std::set& s) { - std::ostringstream oss; - for (auto it = s.begin(); it != s.end(); ++it) { - if (it != s.begin()) { - oss << ','; - } - oss << *it; - } - return oss.str(); - }; - - int major = std::stoi(App::Application::Config()["BuildVersionMajor"]); - int minor = std::stoi(App::Application::Config()["BuildVersionMinor"]); - std::string currentVersionedDirName = App::ApplicationDirectories::versionStringForPath(major, minor); - if (!knownVersions.contains(currentVersionedDirName) - && !App::Application::directories()->usingCurrentVersionConfig( - App::Application::directories()->getUserAppDataDir())) { - auto programName = QString::fromStdString(App::Application::getExecutableName()); - auto result = QMessageBox::question( - mainWindow, - QObject::tr("Welcome to %1 v%2.%3").arg(programName, QString::number(major), QString::number(minor)), - QObject::tr("Welcome to %1 v%2.%3\n\n").arg(programName, QString::number(major), QString::number(minor)) - + QObject::tr("Configuration data and addons from previous program version found. " - "Migrate the old configuration to this version?"), - QMessageBox::Yes | QMessageBox::No); - knownVersions.insert(currentVersionedDirName); - prefGroup->SetASCII("OfferedToMigrateToVersionedConfig", joinCommas(knownVersions)); - if (result == QMessageBox::Yes) { - migrateToCurrentVersion(); - } - } +void StartupPostProcess::checkVersionMigration() const { + VersionMigrator migrator(mainWindow); + migrator.execute(); } - -class PathMigrationWorker : public QObject -{ - Q_OBJECT - -public: - void run () { - try { - App::GetApplication().GetUserParameter().SaveDocument(); - App::Application::directories()->migrateAllPaths( - {App::Application::getUserAppDataDir(), App::Application::getUserConfigPath()}); - Q_EMIT(complete()); - } catch (const Base::Exception& e) { - Base::Console().error("Error migrating configuration data: %s\n", e.what()); - Q_EMIT(failed()); - } catch (const std::exception& e) { - Base::Console().error("Unrecognized error migrating configuration data: %s\n", e.what()); - Q_EMIT(failed()); - } catch (...) { - Base::Console().error("Error migrating configuration data\n"); - Q_EMIT(failed()); - } - } - -Q_SIGNALS: - void complete(); - void failed(); -}; - -void StartupPostProcess::migrateToCurrentVersion() -{ - auto *workerThread = new QThread(mainWindow); - auto *worker = new PathMigrationWorker(); - worker->moveToThread(workerThread); - QObject::connect(workerThread, &QThread::started, worker, &PathMigrationWorker::run); - - auto migrationRunning = new QMessageBox(mainWindow); - migrationRunning->setWindowTitle(QObject::tr("Migrating")); - migrationRunning->setText(QObject::tr("Migrating configuration data and addons...")); - migrationRunning->setStandardButtons(QMessageBox::NoButton); - QObject::connect(worker, &PathMigrationWorker::complete, migrationRunning, &QMessageBox::accept); - QObject::connect(worker, &PathMigrationWorker::failed, migrationRunning, &QMessageBox::reject); - - workerThread->start(); - migrationRunning->exec(); - - if (migrationRunning->result() == QDialog::Accepted) { - auto* restarting = new QMessageBox(mainWindow); - restarting->setText( - QObject::tr("Migration complete. Restarting...")); - restarting->setWindowTitle(QObject::tr("Restarting")); - restarting->setStandardButtons(QMessageBox::NoButton); - auto closeNotice = [restarting]() { - restarting->reject(); - }; - - // Insert a short delay before restart so the user can see the success message, and - // knows it's a restart and not a crash... - const int delayRestartMillis {2000}; - QTimer::singleShot(delayRestartMillis, closeNotice); - restarting->exec(); - - QObject::connect(qApp, &QCoreApplication::aboutToQuit, [=] { - if (getMainWindow()->close()) { - auto args = QApplication::arguments(); - args.removeFirst(); - QProcess::startDetached(QApplication::applicationFilePath(), - args, - QApplication::applicationDirPath()); - } - }); - QCoreApplication::exit(0); - _exit(0); // No really. Die. - } else { - QMessageBox::critical(mainWindow, QObject::tr("Migration failed"),QObject::tr("Migration failed. See the Report View for details.")); - } -} - -#include "StartupProcess.moc" diff --git a/src/Gui/StartupProcess.h b/src/Gui/StartupProcess.h index f3d045d098..82408c725b 100644 --- a/src/Gui/StartupProcess.h +++ b/src/Gui/StartupProcess.h @@ -76,8 +76,7 @@ private: void showMainWindow(); void activateWorkbench(); void checkParameters(); - void runWelcomeScreen(); - void migrateToCurrentVersion(); + void checkVersionMigration() const; private: bool loadFromPythonModule = false; diff --git a/src/Gui/VersionMigrator.cpp b/src/Gui/VersionMigrator.cpp new file mode 100644 index 0000000000..7a9be8b3aa --- /dev/null +++ b/src/Gui/VersionMigrator.cpp @@ -0,0 +1,326 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +/*************************************************************************** + * Copyright (c) 2025 The FreeCAD project association AISBL * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + **************************************************************************/ + +#include "PreCompiled.h" +#ifndef _PreComp_ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#endif + +#include "VersionMigrator.h" + +#include "MainWindow.h" +#include +#include + + +using namespace Gui; +namespace fs = std::filesystem; + + +uintmax_t calculateDirectorySize(const fs::path &dir) { + uintmax_t size = 0; + for (auto &entry: fs::recursive_directory_iterator(dir)) { + if (fs::is_regular_file(entry.status())) { + size += fs::file_size(entry.path()); + } + } + return size; +} + +std::set getKnownVersions() { + auto splitCommas = [](const std::string &input) { + std::set result; + std::stringstream ss(input); + std::string token; + while (std::getline(ss, token, ',')) { + result.insert(token); + } + return result; + }; + + auto prefGroup = App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/Migration"); + + // Split our comma-separated list of already-migrated-to version directories into a set for easy + // searching + std::string offeredToMigrateToVersionedConfig = + prefGroup->GetASCII("OfferedToMigrateToVersionedConfig", ""); + std::set knownVersions; + if (!offeredToMigrateToVersionedConfig.empty()) { + knownVersions = splitCommas(offeredToMigrateToVersionedConfig); + } + return knownVersions; +} + +void setKnownVersions(const std::set &knownVersions) { + auto joinCommas = [](const std::set &s) { + std::ostringstream oss; + for (auto it = s.begin(); it != s.end(); ++it) { + if (it != s.begin()) { + oss << ','; + } + oss << *it; + } + return oss.str(); + }; + + auto prefGroup = App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/Migration"); + prefGroup->SetASCII("OfferedToMigrateToVersionedConfig", joinCommas(knownVersions)); +} + +void markCurrentVersionAsKnown() { + int major = std::stoi(App::Application::Config()["BuildVersionMajor"]); + int minor = std::stoi(App::Application::Config()["BuildVersionMinor"]); + std::string currentVersionedDirName = App::ApplicationDirectories::versionStringForPath(major, minor); + std::set knownVersions = getKnownVersions(); + knownVersions.insert(currentVersionedDirName); + setKnownVersions(knownVersions); +} + +VersionMigrator::VersionMigrator(MainWindow *mw) : QObject(mw), mainWindow(mw) { +} + +void VersionMigrator::execute() { + // If the user is running a custom directory set, there is no migration to versioned directories + if (App::Application::directories()->usingCustomDirectories()) { + return; + } + + auto prefGroup = App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/Migration"); + + // Split our comma-separated list of already-migrated-to version directories into a set for easy + // searching + std::string offeredToMigrateToVersionedConfig = + prefGroup->GetASCII("OfferedToMigrateToVersionedConfig", ""); + std::set knownVersions = getKnownVersions(); + + int major = std::stoi(App::Application::Config()["BuildVersionMajor"]); + int minor = std::stoi(App::Application::Config()["BuildVersionMinor"]); + std::string currentVersionedDirName = App::ApplicationDirectories::versionStringForPath(major, minor); + if (!knownVersions.contains(currentVersionedDirName) + && !App::Application::directories()->usingCurrentVersionConfig( + App::Application::directories()->getUserAppDataDir())) { + auto programName = QString::fromStdString(App::Application::getExecutableName()); + auto result = QMessageBox::question( + mainWindow, + QObject::tr("Welcome to %1 v%2.%3").arg(programName, QString::number(major), QString::number(minor)), + QObject::tr("Welcome to %1 v%2.%3\n\n").arg(programName, QString::number(major), QString::number(minor)) + + QObject::tr("Configuration data and addons from previous program version found. " + "Migrate the configuration to a new directory for this version? Answering 'No' will " + "continue to use the old directory. 'Yes' will copy it."), + QMessageBox::Yes | QMessageBox::No); + if (result == QMessageBox::Yes) { + confirmMigration(); + } else { + // Don't ask again for this version + markCurrentVersionAsKnown(); + } + } +} + +class DirectorySizeCalculationWorker : public QObject { + Q_OBJECT + +public: + void run() { + auto dir = App::Application::directories()->getUserAppDataDir(); + uintmax_t size = 0; + auto thisThread = QThread::currentThread(); + for (auto &entry: fs::recursive_directory_iterator(dir)) { + if (thisThread->isInterruptionRequested()) { + Q_EMIT(cancelled()); + Q_EMIT(finished()); + return; + } + if (fs::is_regular_file(entry.status())) { + size += fs::file_size(entry.path()); + } + } + Q_EMIT(sizeFound(size)); + Q_EMIT(finished()); + } + +Q_SIGNALS: + void finished(); + + void sizeFound(uintmax_t _t1); + + void cancelled(); +}; + +class PathMigrationWorker : public QObject { + Q_OBJECT + +public: + void run() { + try { + App::GetApplication().GetUserParameter().SaveDocument(); + App::Application::directories()->migrateAllPaths( + { + App::Application::directories()->getUserAppDataDir(), + App::Application::directories()->getUserConfigPath() + }); + Q_EMIT(complete()); + } catch (const Base::Exception &e) { + Base::Console().error("Error migrating configuration data: %s\n", e.what()); + Q_EMIT(failed()); + } catch (const std::exception &e) { + Base::Console().error("Unrecognized error migrating configuration data: %s\n", e.what()); + Q_EMIT(failed()); + } catch (...) { + Base::Console().error("Error migrating configuration data\n"); + Q_EMIT(failed()); + } + Q_EMIT(finished()); + } + +Q_SIGNALS: + void finished(); + + void complete(); + + void failed(); +}; + +void VersionMigrator::confirmMigration() { + auto *workerThread = new QThread(mainWindow); + auto *worker = new DirectorySizeCalculationWorker(); + worker->moveToThread(workerThread); + connect(workerThread, &QThread::started, worker, &DirectorySizeCalculationWorker::run); + + connect(worker, &DirectorySizeCalculationWorker::sizeFound, this, &VersionMigrator::showSizeOfMigration); + connect(worker, &DirectorySizeCalculationWorker::finished, workerThread, &QThread::quit); + connect(worker, &DirectorySizeCalculationWorker::finished, worker, &QObject::deleteLater); + connect(workerThread, &QThread::finished, workerThread, &QObject::deleteLater); + + auto calculatingSize = new QMessageBox(mainWindow); + calculatingSize->setWindowTitle(QObject::tr("Calculating size")); + calculatingSize->setText(QObject::tr("Calculating directory size…")); + calculatingSize->setStandardButtons(QMessageBox::Cancel); + connect(worker, &DirectorySizeCalculationWorker::sizeFound, calculatingSize, &QMessageBox::accept); + connect(calculatingSize, &QMessageBox::rejected, workerThread, &QThread::requestInterruption); + connect(calculatingSize, &QMessageBox::rejected, this, &VersionMigrator::migrationCancelled); + + workerThread->start(); + calculatingSize->exec(); +} + +void VersionMigrator::migrationCancelled() const { + auto message = QObject::tr( + "Migration cancelled. Ask again on next restart?"); + auto result = QMessageBox::question( + mainWindow, + QObject::tr("Migration cancelled"), + message, + QMessageBox::Yes | QMessageBox::No); + if (result == QMessageBox::No) { + markCurrentVersionAsKnown(); + } +} + +void VersionMigrator::showSizeOfMigration(uintmax_t size) { + auto sizeString = QLocale().formattedDataSize(static_cast(size)); + auto result = QMessageBox::question( + mainWindow, + QObject::tr("Migration Size"), + QObject::tr("Migrating will copy %1 into a versioned subdirectory. Continue?").arg(sizeString), + QMessageBox::Yes | QMessageBox::No + ); + if (result == QMessageBox::Yes) { + migrateToCurrentVersion(); + } else { + migrationCancelled(); + } +} + +void VersionMigrator::migrateToCurrentVersion() { + auto oldKnownVersions = getKnownVersions(); + markCurrentVersionAsKnown(); // This MUST be done before the migration, or it won't get remembered + auto *workerThread = new QThread(mainWindow); + auto *worker = new PathMigrationWorker(); + worker->moveToThread(workerThread); + connect(workerThread, &QThread::started, worker, &PathMigrationWorker::run); + connect(worker, &PathMigrationWorker::finished, workerThread, &QThread::quit); + connect(worker, &PathMigrationWorker::finished, worker, &QObject::deleteLater); + connect(workerThread, &QThread::finished, workerThread, &QObject::deleteLater); + + auto migrationRunning = new QMessageBox(mainWindow); + migrationRunning->setWindowTitle(QObject::tr("Migrating")); + migrationRunning->setText(QObject::tr("Migrating configuration data and addons...")); + migrationRunning->setStandardButtons(QMessageBox::NoButton); + connect(worker, &PathMigrationWorker::complete, migrationRunning, &QMessageBox::accept); + connect(worker, &PathMigrationWorker::failed, migrationRunning, &QMessageBox::reject); + + workerThread->start(); + migrationRunning->exec(); + + if (migrationRunning->result() == QDialog::Accepted) { + App::GetApplication().GetUserParameter().SaveDocument(); // Flush to disk before restarting + auto *restarting = new QMessageBox(mainWindow); + restarting->setText( + QObject::tr("Migration complete. Restarting...")); + restarting->setWindowTitle(QObject::tr("Restarting")); + restarting->setStandardButtons(QMessageBox::NoButton); + auto closeNotice = [restarting]() { + restarting->reject(); + }; + + // Insert a short delay before restart so the user can see the success message, and + // knows it's a restart and not a crash... + constexpr int delayRestartMillis{2000}; + QTimer::singleShot(delayRestartMillis, closeNotice); + restarting->exec(); + + connect(qApp, &QCoreApplication::aboutToQuit, [=] { + if (getMainWindow()->close()) { + auto args = QApplication::arguments(); + args.removeFirst(); + QProcess::startDetached(QApplication::applicationFilePath(), + args, + QApplication::applicationDirPath()); + } + }); + QCoreApplication::exit(0); + _exit(0); // No really. Die. + } else { + setKnownVersions(oldKnownVersions); // Reset, we didn't migrate after all + QMessageBox::critical(mainWindow, QObject::tr("Migration failed"), + QObject::tr("Migration failed. See the Report View for details.")); + } +} + +#include "VersionMigrator.moc" diff --git a/src/Gui/VersionMigrator.h b/src/Gui/VersionMigrator.h new file mode 100644 index 0000000000..36d90e560b --- /dev/null +++ b/src/Gui/VersionMigrator.h @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +/*************************************************************************** + * Copyright (c) 2025 The FreeCAD project association AISBL * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + **************************************************************************/ + +#ifndef GUI_VERSIONMIGRATOR_H +#define GUI_VERSIONMIGRATOR_H + +#include +#include +#include + +namespace Gui { + + class MainWindow; + + +class GuiExport VersionMigrator : public QObject +{ + Q_OBJECT + +public: + explicit VersionMigrator(MainWindow *mw); + void execute(); + +protected Q_SLOTS: + + void confirmMigration(); + void migrationCancelled() const; + void showSizeOfMigration(uintmax_t size); + void migrateToCurrentVersion(); + +private: + MainWindow* mainWindow; +}; + + +} + +#endif // GUI_VERSIONMIGRATOR_H