ThreeMFWorkspaceWriter.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. # Copyright (c) 2020 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from typing import Optional
  4. import configparser
  5. from io import StringIO
  6. from threading import Lock
  7. import zipfile
  8. from typing import Dict, Any
  9. from pathlib import Path
  10. from zipfile import ZipFile
  11. from UM.Application import Application
  12. from UM.Logger import Logger
  13. from UM.PluginRegistry import PluginRegistry
  14. from UM.Preferences import Preferences
  15. from UM.Settings.ContainerRegistry import ContainerRegistry
  16. from UM.Workspace.WorkspaceWriter import WorkspaceWriter
  17. from UM.i18n import i18nCatalog
  18. catalog = i18nCatalog("cura")
  19. from .ThreeMFWriter import ThreeMFWriter
  20. from .SettingsExportModel import SettingsExportModel
  21. from .SettingsExportGroup import SettingsExportGroup
  22. USER_SETTINGS_PATH = "Cura/user-settings.json"
  23. class ThreeMFWorkspaceWriter(WorkspaceWriter):
  24. def __init__(self):
  25. super().__init__()
  26. self._ucp_model: Optional[SettingsExportModel] = None
  27. def setExportModel(self, model: SettingsExportModel) -> None:
  28. if self._ucp_model != model:
  29. self._ucp_model = model
  30. def _write(self, stream, nodes, mode, include_log):
  31. application = Application.getInstance()
  32. machine_manager = application.getMachineManager()
  33. mesh_writer = application.getMeshFileHandler().getWriter("3MFWriter")
  34. if not mesh_writer: # We need to have the 3mf mesh writer, otherwise we can't save the entire workspace
  35. self.setInformation(catalog.i18nc("@error:zip", "3MF Writer plug-in is corrupt."))
  36. Logger.error("3MF Writer class is unavailable. Can't write workspace.")
  37. return False
  38. global_stack = machine_manager.activeMachine
  39. if global_stack is None:
  40. self.setInformation(
  41. catalog.i18nc("@error", "There is no workspace yet to write. Please add a printer first."))
  42. Logger.error("Tried to write a 3MF workspace before there was a global stack.")
  43. return False
  44. # Indicate that the 3mf mesh writer should not close the archive just yet (we still need to add stuff to it).
  45. mesh_writer.setStoreArchive(True)
  46. if not mesh_writer.write(stream, nodes, mode, self._ucp_model):
  47. self.setInformation(mesh_writer.getInformation())
  48. return False
  49. archive = mesh_writer.getArchive()
  50. if archive is None: # This happens if there was no mesh data to write.
  51. archive = zipfile.ZipFile(stream, "w", compression=zipfile.ZIP_DEFLATED)
  52. try:
  53. # Add global container stack data to the archive.
  54. self._writeContainerToArchive(global_stack, archive)
  55. # Also write all containers in the stack to the file
  56. for container in global_stack.getContainers():
  57. self._writeContainerToArchive(container, archive)
  58. # Check if the machine has extruders and save all that data as well.
  59. for extruder_stack in global_stack.extruderList:
  60. self._writeContainerToArchive(extruder_stack, archive)
  61. for container in extruder_stack.getContainers():
  62. self._writeContainerToArchive(container, archive)
  63. # Write user settings data
  64. if self._ucp_model is not None:
  65. user_settings_data = self._getUserSettings(self._ucp_model)
  66. ThreeMFWriter._storeMetadataJson(user_settings_data, archive, USER_SETTINGS_PATH)
  67. # Write log file
  68. if include_log:
  69. ThreeMFWorkspaceWriter._writeLogFile(archive)
  70. except PermissionError:
  71. self.setInformation(catalog.i18nc("@error:zip", "No permission to write the workspace here."))
  72. Logger.error("No permission to write workspace to this stream.")
  73. return False
  74. # Write preferences to archive
  75. original_preferences = Application.getInstance().getPreferences() # Copy only the preferences that we use to the workspace.
  76. temp_preferences = Preferences()
  77. for preference in {"general/visible_settings", "cura/active_mode", "cura/categories_expanded",
  78. "metadata/setting_version"}:
  79. temp_preferences.addPreference(preference, None)
  80. temp_preferences.setValue(preference, original_preferences.getValue(preference))
  81. preferences_string = StringIO()
  82. temp_preferences.writeToFile(preferences_string)
  83. preferences_file = zipfile.ZipInfo("Cura/preferences.cfg")
  84. try:
  85. archive.writestr(preferences_file, preferences_string.getvalue())
  86. # Save Cura version
  87. version_file = zipfile.ZipInfo("Cura/version.ini")
  88. version_config_parser = configparser.ConfigParser(interpolation=None)
  89. version_config_parser.add_section("versions")
  90. version_config_parser.set("versions", "cura_version", application.getVersion())
  91. version_config_parser.set("versions", "build_type", application.getBuildType())
  92. version_config_parser.set("versions", "is_debug_mode", str(application.getIsDebugMode()))
  93. version_file_string = StringIO()
  94. version_config_parser.write(version_file_string)
  95. archive.writestr(version_file, version_file_string.getvalue())
  96. self._writePluginMetadataToArchive(archive)
  97. # Close the archive & reset states.
  98. archive.close()
  99. except PermissionError:
  100. self.setInformation(catalog.i18nc("@error:zip", "No permission to write the workspace here."))
  101. Logger.error("No permission to write workspace to this stream.")
  102. return False
  103. except EnvironmentError as e:
  104. self.setInformation(catalog.i18nc("@error:zip", str(e)))
  105. Logger.error("EnvironmentError when writing workspace to this stream: {err}".format(err=str(e)))
  106. return False
  107. mesh_writer.setStoreArchive(False)
  108. return True
  109. def write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode, **kwargs):
  110. success = self._write(stream, nodes, WorkspaceWriter.OutputMode.BinaryMode, kwargs.get("include_log", False))
  111. self._ucp_model = None
  112. return success
  113. @staticmethod
  114. def _writePluginMetadataToArchive(archive: zipfile.ZipFile) -> None:
  115. file_name_template = "%s/plugin_metadata.json"
  116. for plugin_id, metadata in Application.getInstance().getWorkspaceMetadataStorage().getAllData().items():
  117. file_name = file_name_template % plugin_id
  118. file_in_archive = zipfile.ZipInfo(file_name)
  119. # We have to set the compress type of each file as well (it doesn't keep the type of the entire archive)
  120. file_in_archive.compress_type = zipfile.ZIP_DEFLATED
  121. import json
  122. archive.writestr(file_in_archive, json.dumps(metadata, separators = (", ", ": "), indent = 4, skipkeys = True))
  123. @staticmethod
  124. def _writeContainerToArchive(container, archive):
  125. """Helper function that writes ContainerStacks, InstanceContainers and DefinitionContainers to the archive.
  126. :param container: That follows the :type{ContainerInterface} to archive.
  127. :param archive: The archive to write to.
  128. """
  129. if isinstance(container, type(ContainerRegistry.getInstance().getEmptyInstanceContainer())):
  130. return # Empty file, do nothing.
  131. file_suffix = ContainerRegistry.getMimeTypeForContainer(type(container)).preferredSuffix
  132. # Some containers have a base file, which should then be the file to use.
  133. if "base_file" in container.getMetaData():
  134. base_file = container.getMetaDataEntry("base_file")
  135. if base_file != container.getId():
  136. container = ContainerRegistry.getInstance().findContainers(id = base_file)[0]
  137. file_name = "Cura/%s.%s" % (container.getId(), file_suffix)
  138. try:
  139. if file_name in archive.namelist():
  140. return # File was already saved, no need to do it again. Uranium guarantees unique ID's, so this should hold.
  141. file_in_archive = zipfile.ZipInfo(file_name)
  142. # For some reason we have to set the compress type of each file as well (it doesn't keep the type of the entire archive)
  143. file_in_archive.compress_type = zipfile.ZIP_DEFLATED
  144. # Do not include the network authentication keys
  145. ignore_keys = {
  146. "um_cloud_cluster_id",
  147. "um_network_key",
  148. "um_linked_to_account",
  149. "removal_warning",
  150. "host_guid",
  151. "group_name",
  152. "group_size",
  153. "connection_type",
  154. "capabilities",
  155. "octoprint_api_key",
  156. "is_online",
  157. }
  158. serialized_data = container.serialize(ignored_metadata_keys = ignore_keys)
  159. archive.writestr(file_in_archive, serialized_data)
  160. except (FileNotFoundError, EnvironmentError):
  161. Logger.error("File became inaccessible while writing to it: {archive_filename}".format(archive_filename = archive.fp.name))
  162. return
  163. @staticmethod
  164. def _writeLogFile(archive: ZipFile) -> None:
  165. """Helper function that writes the Cura log file to the archive.
  166. :param archive: The archive to write to.
  167. """
  168. file_logger = PluginRegistry.getInstance().getPluginObject("FileLogger")
  169. file_logger.flush()
  170. for file_path in file_logger.getFilesPaths():
  171. archive.write(file_path, arcname=f"log/{Path(file_path).name}")
  172. @staticmethod
  173. def _getUserSettings(model: SettingsExportModel) -> Dict[str, Dict[str, Any]]:
  174. user_settings = {}
  175. for group in model.settingsGroups:
  176. category = ''
  177. if group.category == SettingsExportGroup.Category.Global:
  178. category = 'global'
  179. elif group.category == SettingsExportGroup.Category.Extruder:
  180. category = f"extruder_{group.extruder_index}"
  181. if len(category) > 0:
  182. settings_values = {}
  183. stack = group.stack
  184. for setting in group.settings:
  185. if setting.selected:
  186. settings_values[setting.id] = stack.getProperty(setting.id, "value")
  187. user_settings[category] = settings_values
  188. return user_settings