From 260aa3abf14aef7d28eb9621df79345a3daeb017 Mon Sep 17 00:00:00 2001 From: sliptonic Date: Sat, 20 Sep 2025 10:02:45 -0500 Subject: [PATCH] refactor migration --- src/Mod/CAM/CMakeLists.txt | 13 ++ src/Mod/CAM/InitGui.py | 122 +--------- src/Mod/CAM/Path/Preferences.py | 15 +- .../CAM/Path/Tool/assets/ui/preferences.py | 9 +- src/Mod/CAM/Path/Tool/camassets.py | 7 +- .../CAM/Path/Tool/library/serializers/fctl.py | 6 +- src/Mod/CAM/Path/Tool/migration/__init__.py | 32 +++ src/Mod/CAM/Path/Tool/migration/migration.py | 220 ++++++++++++++++++ 8 files changed, 296 insertions(+), 128 deletions(-) create mode 100644 src/Mod/CAM/Path/Tool/migration/__init__.py create mode 100644 src/Mod/CAM/Path/Tool/migration/migration.py diff --git a/src/Mod/CAM/CMakeLists.txt b/src/Mod/CAM/CMakeLists.txt index 5cc41ed1b3..6e965b361a 100644 --- a/src/Mod/CAM/CMakeLists.txt +++ b/src/Mod/CAM/CMakeLists.txt @@ -128,6 +128,11 @@ SET(PathPythonToolsAssets_SRCS Path/Tool/assets/uri.py ) +SET(PathPythonToolsMigration_SRCS + Path/Tool/migration/__init__.py + Path/Tool/migration/migration.py +) + SET(PathPythonToolsAssetsStore_SRCS Path/Tool/assets/store/__init__.py Path/Tool/assets/store/base.py @@ -600,6 +605,7 @@ SET(all_files ${PathPythonPostScripts_SRCS} ${PathPythonTools_SRCS} ${PathPythonToolsAssets_SRCS} + ${PathPythonToolsMigration_SRCS} ${PathPythonToolsAssetsStore_SRCS} ${PathPythonToolsAssetsUi_SRCS} ${PathPythonToolsDocObject_SRCS} @@ -755,6 +761,13 @@ INSTALL( Mod/CAM/Path/Tool/assets ) +INSTALL( + FILES + ${PathPythonToolsMigration_SRCS} + DESTINATION + Mod/CAM/Path/Tool/migration +) + INSTALL( FILES ${PathPythonToolsAssetsStore_SRCS} diff --git a/src/Mod/CAM/InitGui.py b/src/Mod/CAM/InitGui.py index 24bcaac8c9..38b10015f5 100644 --- a/src/Mod/CAM/InitGui.py +++ b/src/Mod/CAM/InitGui.py @@ -22,15 +22,10 @@ # * * # *************************************************************************** import FreeCAD -import Path.Preferences - -import shutil -import pathlib if FreeCAD.GuiUp: import FreeCADGui from FreeCADGui import Workbench - from PySide.QtWidgets import QMessageBox else: # Provide a dummy Workbench class when GUI is not available class Workbench: @@ -39,32 +34,6 @@ else: FreeCAD.__unit_test__ += ["TestCAMGui"] - -# if FreeCAD.GuiUp: -# import FreeCADGui -# import Path.Base.Gui.PreferencesAdvanced as PathPreferencesAdvanced -# import Path.Base.Gui.PreferencesJob as PathPreferencesPathJob -# import Path.Dressup.Gui.Preferences as PathPreferencesPathDressup -# import Path.Tool.assets.ui.preferences as AssetPreferences - -# FreeCADGui.addPreferencePage( -# PathPreferencesPathJob.JobPreferencesPage, -# QT_TRANSLATE_NOOP("QObject", "CAM"), -# ) -# FreeCADGui.addPreferencePage( -# AssetPreferences.AssetPreferencesPage, -# QT_TRANSLATE_NOOP("QObject", "CAM"), -# ) -# FreeCADGui.addPreferencePage( -# PathPreferencesPathDressup.DressupPreferencesPage, -# QT_TRANSLATE_NOOP("QObject", "CAM"), -# ) -# FreeCADGui.addPreferencePage( -# PathPreferencesAdvanced.AdvancedPreferencesPage, -# QT_TRANSLATE_NOOP("QObject", "CAM"), -# ) - - class PathCommandGroup: def __init__(self, cmdlist, menu, tooltip=None): self.cmdlist = cmdlist @@ -96,93 +65,6 @@ class CAMWorkbench(Workbench): self.__class__.MenuText = "CAM" self.__class__.ToolTip = "CAM workbench" - def _migrate_cam_assets(self, current_version, cam_asset_path): - """ - Migrate CAM assets to a versioned directory. - """ - try: - # Create versioned directory name - cam_asset_path = pathlib.Path(cam_asset_path) - parent_dir = cam_asset_path.parent - versioned_name = f"{cam_asset_path.name}_{current_version}" - versioned_path = parent_dir / versioned_name - - # Copy the existing assets to the versioned directory - if cam_asset_path.exists(): - shutil.copytree(cam_asset_path, versioned_path, dirs_exist_ok=True) - - # Update the CAM asset preference to point to the new location - Path.Preferences.setAssetPath(str(versioned_path)) - - Path.Log.info(f"CAM assets migrated to versioned directory: {versioned_path}") - else: - Path.Log.warning(f"CAM asset path does not exist: {cam_asset_path}") - - except Exception as e: - Path.Log.error(f"Failed to migrate CAM assets: {e}") - if FreeCAD.GuiUp: - QMessageBox.critical( - FreeCADGui.getMainWindow(), - "CAM Asset Migration Failed", - f"Failed to migrate CAM assets: {str(e)}" - ) - - def _offer_cam_asset_migration(self, current_version, cam_asset_path, pref_group, known_versions): - """ - Offer CAM asset migration to the user. - """ - if not FreeCAD.GuiUp: - return - - result = QMessageBox.question( - FreeCADGui.getMainWindow(), - "CAM Asset Migration", - f"CAM Assets are stored in a custom location:\n{cam_asset_path}\n\n" - f"Would you like to create a versioned copy for FreeCAD v{current_version}?", - QMessageBox.Yes | QMessageBox.No - ) - - # Record that we offered migration for this version - known_versions.add(current_version) - pref_group.SetASCII("OfferedToMigrateCAMAssets", ','.join(known_versions)) - - if result == QMessageBox.Yes: - self._migrate_cam_assets(current_version, cam_asset_path) - - def _check_cam_asset_migration(self): - """ - Check if CAM asset migration is needed for version upgrade. - """ - try: - # Only proceed if not using custom directories (same as main system) - if FreeCAD.Application.directories().usingCustomDirectories(): - return - - # Get the current CAM asset path - cam_asset_path = Path.Preferences.getAssetPath() - user_app_data_dir = pathlib.Path(FreeCAD.getUserAppDataDir()) - - # Only migrate if CamAssets is outside the standard user data directory - if pathlib.Path(cam_asset_path).is_relative_to(user_app_data_dir): - return # CamAssets is in default location, no custom migration needed - - # Check if we've already offered migration for this version - pref_group = FreeCAD.GetApplication().GetParameterGroupByPath( - "User parameter:BaseApp/Preferences/CAM/Migration") - - major = int(FreeCAD.Application.Config()["BuildVersionMajor"]) - minor = int(FreeCAD.Application.Config()["BuildVersionMinor"]) - current_version = FreeCAD.ApplicationDirectories.versionStringForPath(major, minor) - - offered_versions = pref_group.GetASCII("OfferedToMigrateCAMAssets", "") - known_versions = set(offered_versions.split(',')) if offered_versions else set() - - if current_version not in known_versions: - # Offer migration - self._offer_cam_asset_migration(current_version, cam_asset_path, pref_group, known_versions) - - except Exception as e: - Path.Log.error(f"Error checking CAM asset migration: {e}") def Initialize(self): global PathCommandGroup @@ -214,7 +96,9 @@ class CAMWorkbench(Workbench): cam_assets.setup() # Check if CAM asset migration is needed for version upgrade - self._check_cam_asset_migration() + from Path.Tool.migration.migration import CAMAssetMigrator + migrator = CAMAssetMigrator() + migrator.check_migration_needed() from PySide.QtCore import QT_TRANSLATE_NOOP diff --git a/src/Mod/CAM/Path/Preferences.py b/src/Mod/CAM/Path/Preferences.py index 6cb04c7cd9..83e475863d 100644 --- a/src/Mod/CAM/Path/Preferences.py +++ b/src/Mod/CAM/Path/Preferences.py @@ -29,7 +29,7 @@ from collections import defaultdict from typing import Optional -if False: +if True: Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) Path.Log.trackModule(Path.Log.thisModule()) else: @@ -128,7 +128,9 @@ def getAssetPath() -> pathlib.Path: # Check if we have a CamAssets path already set cam_assets_path = pref.GetString(ToolPath, "") if cam_assets_path: - return pathlib.Path(cam_assets_path) + # Use mostRecentConfigFromBase to get the most recent versioned path + most_recent_path = FreeCAD.ApplicationDirectories.mostRecentConfigFromBase(cam_assets_path) + return pathlib.Path(most_recent_path) # Migration: Check for legacy DefaultFilePath and use it for CamAssets legacy_path = defaultFilePath() @@ -137,14 +139,19 @@ def getAssetPath() -> pathlib.Path: if legacy_path_obj.exists() and legacy_path_obj.is_dir(): # Migrate: Set the legacy path as the new CamAssets path setAssetPath(legacy_path_obj) - return legacy_path_obj + # Return the most recent version of the legacy path + most_recent_legacy = FreeCAD.ApplicationDirectories.mostRecentConfigFromBase(str(legacy_path_obj)) + return pathlib.Path(most_recent_legacy) # Fallback to default if no legacy path found default = getDefaultAssetPath() - return pathlib.Path(default) + # Return the most recent version of the default path + most_recent_default = FreeCAD.ApplicationDirectories.mostRecentConfigFromBase(str(default)) + return pathlib.Path(most_recent_default) def setAssetPath(path: pathlib.Path): + Path.Log.info(f"Setting asset path to {path}") assert path.is_dir(), f"Cannot put a non-initialized asset directory into preferences: {path}" pref = tool_preferences() current_path = pref.GetString(ToolPath, "") diff --git a/src/Mod/CAM/Path/Tool/assets/ui/preferences.py b/src/Mod/CAM/Path/Tool/assets/ui/preferences.py index e74305ad05..2e322c6004 100644 --- a/src/Mod/CAM/Path/Tool/assets/ui/preferences.py +++ b/src/Mod/CAM/Path/Tool/assets/ui/preferences.py @@ -124,6 +124,9 @@ class AssetPreferencesPage: return True def loadSettings(self): - # use getAssetPath() to initialize UI - asset_path = Path.Preferences.getAssetPath() - self.asset_path_edit.setText(str(asset_path)) + # Get the raw preference value, not the versioned path + pref = Path.Preferences.tool_preferences() + asset_path = pref.GetString(Path.Preferences.ToolPath, "") + if not asset_path: + asset_path = str(Path.Preferences.getDefaultAssetPath()) + self.asset_path_edit.setText(asset_path) \ No newline at end of file diff --git a/src/Mod/CAM/Path/Tool/camassets.py b/src/Mod/CAM/Path/Tool/camassets.py index 8bffbeaec3..a6695e2261 100644 --- a/src/Mod/CAM/Path/Tool/camassets.py +++ b/src/Mod/CAM/Path/Tool/camassets.py @@ -27,6 +27,11 @@ from Path import Preferences from Path.Preferences import addToolPreferenceObserver from .assets import AssetManager, AssetUri, Asset, FileStore +if True: + Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) + Path.Log.trackModule(Path.Log.thisModule()) +else: + Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) def ensure_library_assets_initialized(asset_manager: AssetManager, store_name: str = "local"): """ @@ -151,7 +156,7 @@ def ensure_assets_initialized(asset_manager: AssetManager, store="local"): def _on_asset_path_changed(group, key, value): Path.Log.info(f"CAM asset directory changed in preferences: {group} {key} {value}") - user_asset_store.set_dir(Preferences.getAssetPath()) + user_asset_store.set_dir(value) ensure_assets_initialized(cam_assets) diff --git a/src/Mod/CAM/Path/Tool/library/serializers/fctl.py b/src/Mod/CAM/Path/Tool/library/serializers/fctl.py index 857db214c7..37c8cd0d7d 100644 --- a/src/Mod/CAM/Path/Tool/library/serializers/fctl.py +++ b/src/Mod/CAM/Path/Tool/library/serializers/fctl.py @@ -72,7 +72,11 @@ class FCTLSerializer(AssetSerializer): # for the asset being deserialized. We should use this ID for the library # instance, overriding any 'id' that might be in the data_dict (which # is from an older version of the format). - library = Library(data_dict.get("label", id or "Unnamed Library"), id=id) + + # For the label, prefer data_dict["label"], then "name", then fallback to "Unnamed Library" + # Avoid using the UUID as the library name + label = data_dict.get("label") or data_dict.get("name") or "Unnamed Library" + library = Library(label, id=id) if dependencies is None: Path.Log.debug( diff --git a/src/Mod/CAM/Path/Tool/migration/__init__.py b/src/Mod/CAM/Path/Tool/migration/__init__.py new file mode 100644 index 0000000000..41501293ad --- /dev/null +++ b/src/Mod/CAM/Path/Tool/migration/__init__.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: LGPL-2.1-or-later +# *************************************************************************** +# * * +# * Copyright (c) 2025 sliptonic * +# * * +# * This file is part of FreeCAD. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program 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 Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +""" +CAM Asset management package. + +This package contains modules for managing CAM assets including migration +functionality for handling asset directory versioning during FreeCAD upgrades. +""" diff --git a/src/Mod/CAM/Path/Tool/migration/migration.py b/src/Mod/CAM/Path/Tool/migration/migration.py new file mode 100644 index 0000000000..c7352ef8e0 --- /dev/null +++ b/src/Mod/CAM/Path/Tool/migration/migration.py @@ -0,0 +1,220 @@ +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: LGPL-2.1-or-later +# *************************************************************************** +# * * +# * Copyright (c) 2025 sliptonic * +# * * +# * 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 * +# * . * +# * * +# *************************************************************************** +""" +CAM Asset Migration Module + +Handles migration of CAM assets during FreeCAD version upgrades. +""" + +import FreeCAD +import Path +import Path.Preferences +import pathlib + +# Logging setup - same pattern as Job.py +if True: + Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) + Path.Log.trackModule(Path.Log.thisModule()) +else: + Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) + +if FreeCAD.GuiUp: + import FreeCADGui + from PySide.QtWidgets import QMessageBox + + +class CAMAssetMigrator: + """ + Handles migration of CAM assets during FreeCAD version upgrades. + + This class provides functionality to: + - Check if migration is needed for custom CAM asset locations + - Offer migration to users through a dialog + - Perform the actual asset migration with versioned directories + """ + + def __init__(self): + self.pref_group_path = "User parameter:BaseApp/Preferences/Mod/CAM/Migration" + + def check_migration_needed(self): + """ + Public method to check and handle CAM asset migration. + """ + self._check_migration_needed() + + def _check_migration_needed(self): + """ + Check if CAM asset migration is needed for version upgrade. + + This method determines if the current CAM assets are stored in a custom + location outside the default user data directory and if migration has + not been offered for the current FreeCAD version. + + Returns: + bool: True if migration should be offered, False otherwise + """ + Path.Log.info("Starting CAM asset migration check") + + try: + # Get current directories + user_app_data_dir = FreeCAD.getUserAppDataDir() + user_app_data_path = pathlib.Path(user_app_data_dir) + Path.Log.debug(f"User app data directory: {user_app_data_dir}") + + # Get the current CAM asset path (may be naked or versioned) + current_asset_path = Path.Preferences.getAssetPath() + current_asset_pathlib = pathlib.Path(current_asset_path) + Path.Log.debug(f"Current CAM asset path: {current_asset_path}") + + # Only migrate if CamAssets is outside the standard user data directory + if current_asset_pathlib.is_relative_to(user_app_data_path): + Path.Log.debug("CamAssets is in default location, no custom migration needed") + return + + # Check if migration has already been offered for this version + if self.has_migration_been_offered(): + Path.Log.debug("Migration has already been offered for this version, skipping") + return + + # Determine the base path (naked path without version) + if FreeCAD.ApplicationDirectories.isVersionedPath(str(current_asset_path)): + # Check if we're already using the current version + if FreeCAD.ApplicationDirectories.usingCurrentVersionConfig(str(current_asset_path)): + Path.Log.debug("Already using current version, no migration needed") + return + + Path.Log.info("Migration is needed and should be offered") + if self._offer_migration_to_user(): + self._migrate_assets(str(current_asset_path)) + return + + except Exception as e: + Path.Log.error(f"Error checking CAM asset migration: {e}") + import traceback + Path.Log.debug(f"Full traceback: {traceback.format_exc()}") + return False + + def _offer_migration_to_user(self): + """ + Present migration dialog to user. + + Returns: + bool: True if user accepted migration, False otherwise + """ + # Get current version info + major = int(FreeCAD.ConfigGet("BuildVersionMajor")) + minor = int(FreeCAD.ConfigGet("BuildVersionMinor")) + current_version = FreeCAD.ApplicationDirectories.versionStringForPath(major, minor) + + # Get current asset path for display + current_asset_path = Path.Preferences.getAssetPath() + + Path.Log.debug(f"Offering migration to user for version {current_version}") + + if not FreeCAD.GuiUp: + Path.Log.debug("GUI not available, skipping migration offer") + return False + + msg = ( + f"FreeCAD has been upgraded to version {current_version}.\n\n" + f"Your CAM assets are stored in a custom location:\n{current_asset_path}\n\n" + "Would you like to migrate your CAM assets to a versioned directory " + "to preserve them during future upgrades?\n\n" + "This will copy your assets to a new directory." + ) + + Path.Log.debug("Showing migration dialog to user") + + reply = QMessageBox.question( + None, + "CAM Asset Migration", + msg, + QMessageBox.Yes | QMessageBox.No, + QMessageBox.Yes + ) + + # Record that we offered migration for this version + pref_group = FreeCAD.ParamGet(self.pref_group_path) + offered_versions = pref_group.GetString("OfferedToMigrateCAMAssets", "") + known_versions = set(offered_versions.split(',')) if offered_versions else set() + known_versions.add(current_version) + pref_group.SetString("OfferedToMigrateCAMAssets", ",".join(known_versions)) + Path.Log.debug(f"Updated offered versions: {known_versions}") + + if reply == QMessageBox.Yes: + Path.Log.info("User accepted migration, starting asset migration") + return True + else: + Path.Log.info("User declined migration") + return False + + def _migrate_assets(self, source_path): + """ + Perform actual directory copying and preference updates. + + Args: + source_path: Current CAM asset directory path + """ + Path.Log.info(f"Starting asset migration from {source_path}") + + try: + FreeCAD.ApplicationDirectories.migrateAllPaths([source_path]) + Path.Log.info("Migration complete - preferences will be handled automatically by the system") + + if FreeCAD.GuiUp: + QMessageBox.information( + None, + "Migration Complete", + f"CAM assets have been migrated from:\n{source_path}\n\n" + "The system will automatically handle preference updates." + ) + + except Exception as e: + error_msg = f"Failed to migrate CAM assets: {e}" + Path.Log.error(error_msg) + import traceback + Path.Log.debug(f"Migration error traceback: {traceback.format_exc()}") + if FreeCAD.GuiUp: + QMessageBox.critical( + None, + "Migration Failed", + error_msg + ) + + def has_migration_been_offered(self): + """ + Check if migration has been offered for current version. + + Returns: + bool: True if migration was offered for this version + """ + + pref_group = FreeCAD.ParamGet(self.pref_group_path) + major = int(FreeCAD.ConfigGet("BuildVersionMajor")) + minor = int(FreeCAD.ConfigGet("BuildVersionMinor")) + + current_version_string = FreeCAD.ApplicationDirectories.versionStringForPath(major, minor) + offered_versions = pref_group.GetString("OfferedToMigrateCAMAssets", "") + known_versions = set(offered_versions.split(',')) if offered_versions else set() + return current_version_string in known_versions \ No newline at end of file