CuraPackageManager.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. # Copyright (c) 2023 UltiMaker
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import glob
  4. import os
  5. from pathlib import Path
  6. from typing import Any, cast, Dict, List, Set, Tuple, TYPE_CHECKING, Optional
  7. from UM.Logger import Logger
  8. from UM.PluginRegistry import PluginRegistry
  9. from cura.CuraApplication import CuraApplication # To find some resource types.
  10. from cura.Settings.GlobalStack import GlobalStack
  11. from UM.PackageManager import PackageManager # The class we're extending.
  12. from UM.Resources import Resources # To find storage paths for some resource types.
  13. from UM.i18n import i18nCatalog
  14. from urllib.parse import unquote_plus
  15. catalog = i18nCatalog("cura")
  16. if TYPE_CHECKING:
  17. from UM.Qt.QtApplication import QtApplication
  18. from PyQt6.QtCore import QObject
  19. class CuraPackageManager(PackageManager):
  20. def __init__(self, application: "QtApplication", parent: Optional["QObject"] = None) -> None:
  21. super().__init__(application, parent)
  22. self._local_packages: Optional[List[Dict[str, Any]]] = None
  23. self._local_packages_ids: Optional[Set[str]] = None
  24. self.installedPackagesChanged.connect(self._updateLocalPackages)
  25. def _updateLocalPackages(self) -> None:
  26. self._local_packages = self.getAllLocalPackages()
  27. self._local_packages_ids = set(pkg["package_id"] for pkg in self._local_packages)
  28. @property
  29. def local_packages(self) -> List[Dict[str, Any]]:
  30. """locally installed packages, lazy execution"""
  31. if self._local_packages is None:
  32. self._updateLocalPackages()
  33. # _updateLocalPackages always results in a list of packages, not None.
  34. # It's guaranteed to be a list now.
  35. return cast(List[Dict[str, Any]], self._local_packages)
  36. @property
  37. def local_packages_ids(self) -> Set[str]:
  38. """locally installed packages, lazy execution"""
  39. if self._local_packages_ids is None:
  40. self._updateLocalPackages()
  41. # _updateLocalPackages always results in a list of packages, not None.
  42. # It's guaranteed to be a list now.
  43. return cast(Set[str], self._local_packages_ids)
  44. def initialize(self) -> None:
  45. self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer)
  46. self._installation_dirs_dict["qualities"] = Resources.getStoragePath(CuraApplication.ResourceTypes.QualityInstanceContainer)
  47. self._installation_dirs_dict["variants"] = Resources.getStoragePath(
  48. CuraApplication.ResourceTypes.VariantInstanceContainer)
  49. self._installation_dirs_dict["images"] = Resources.getStoragePath(CuraApplication.ResourceTypes.ImageFiles)
  50. # Due to a bug in Cura 5.1.0 we needed to change the directory structure of the curapackage on the server side (See SD-3871).
  51. # Although the material intent profiles will be installed in the `intent` folder, the curapackage from the server side will
  52. # have an `intents` folder. For completeness, we will look in both locations of in the curapackage and map them both to the
  53. # `intent` folder.
  54. self._installation_dirs_dict["intents"] = Resources.getStoragePath(CuraApplication.ResourceTypes.IntentInstanceContainer)
  55. self._installation_dirs_dict["intent"] = Resources.getStoragePath(CuraApplication.ResourceTypes.IntentInstanceContainer)
  56. super().initialize()
  57. def isMaterialBundled(self, file_name: str, guid: str):
  58. """ Check if there is a bundled material name with file_name and guid """
  59. for path in Resources.getSecureSearchPaths():
  60. # Secure search paths are install directory paths, if a material is in here it must be bundled.
  61. paths = [Path(p) for p in glob.glob(path + '/**/*.xml.fdm_material', recursive=True)]
  62. for material in paths:
  63. if material.name == file_name:
  64. Logger.info(f"Found bundled material: {material.name}. Located in path: {str(material)}")
  65. with open(material, encoding="utf-8") as f:
  66. # Make sure the file we found has the same guid as our material
  67. # Parsing this xml would be better but the namespace is needed to search it.
  68. parsed_guid = PluginRegistry.getInstance().getPluginObject(
  69. "XmlMaterialProfile").getMetadataFromSerialized(
  70. f.read(), "GUID")
  71. if guid == parsed_guid:
  72. # The material we found matches both filename and GUID
  73. return True
  74. return False
  75. def getMaterialFilePackageId(self, file_name: str, guid: str) -> str:
  76. """Get the id of the installed material package that contains file_name"""
  77. file_name = unquote_plus(file_name)
  78. for material_package in [f for f in os.scandir(self._installation_dirs_dict["materials"]) if f.is_dir()]:
  79. package_id = material_package.name
  80. for root, _, file_names in os.walk(material_package.path):
  81. if file_name not in file_names:
  82. # File with the name we are looking for is not in this directory
  83. continue
  84. with open(os.path.join(root, file_name), encoding="utf-8") as f:
  85. # Make sure the file we found has the same guid as our material
  86. # Parsing this xml would be better but the namespace is needed to search it.
  87. parsed_guid = PluginRegistry.getInstance().getPluginObject("XmlMaterialProfile").getMetadataFromSerialized(
  88. f.read(), "GUID")
  89. if guid == parsed_guid:
  90. return package_id
  91. Logger.error("Could not find package_id for file: {} with GUID: {} ".format(file_name, guid))
  92. Logger.error(f"Bundled paths searched: {list(Resources.getSecureSearchPaths())}")
  93. return ""
  94. def getMachinesUsingPackage(self, package_id: str) -> Tuple[List[Tuple[GlobalStack, str, str]], List[Tuple[GlobalStack, str, str]]]:
  95. """Returns a list of where the package is used
  96. It loops through all the package contents and see if some of the ids are used.
  97. :param package_id: package id to search for
  98. :return: empty if it is never used, otherwise a list consisting of 3-tuples
  99. """
  100. ids = self.getPackageContainerIds(package_id)
  101. container_stacks = self._application.getContainerRegistry().findContainerStacks()
  102. global_stacks = [container_stack for container_stack in container_stacks if isinstance(container_stack, GlobalStack)]
  103. machine_with_materials = []
  104. machine_with_qualities = []
  105. for container_id in ids:
  106. for global_stack in global_stacks:
  107. for extruder_nr, extruder_stack in enumerate(global_stack.extruderList):
  108. if container_id in (extruder_stack.material.getId(), extruder_stack.material.getMetaData().get("base_file")):
  109. machine_with_materials.append((global_stack, str(extruder_nr), container_id))
  110. if container_id == extruder_stack.quality.getId():
  111. machine_with_qualities.append((global_stack, str(extruder_nr), container_id))
  112. return machine_with_materials, machine_with_qualities
  113. def getAllLocalPackages(self) -> List[Dict[str, Any]]:
  114. """ Returns an unordered list of all the package_info of installed, to be installed, or bundled packages"""
  115. packages: List[Dict[str, Any]] = []
  116. for packages_to_add in self.getAllInstalledPackagesInfo().values():
  117. packages.extend(packages_to_add)
  118. return packages