ThreeMFWorkspaceWriter.py 11 KB

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