ThreeMFWorkspaceWriter.py 11 KB

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