Backup.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. # Copyright (c) 2018 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import io
  4. import os
  5. import re
  6. import shutil
  7. from typing import Optional
  8. from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile
  9. from UM import i18nCatalog
  10. from UM.Logger import Logger
  11. from UM.Message import Message
  12. from UM.Platform import Platform
  13. from UM.Resources import Resources
  14. from cura.CuraApplication import CuraApplication
  15. ## The back-up class holds all data about a back-up.
  16. #
  17. # It is also responsible for reading and writing the zip file to the user data
  18. # folder.
  19. class Backup:
  20. # These files should be ignored when making a backup.
  21. IGNORED_FILES = [r"cura\.log", r"plugins\.json", r"cache", r"__pycache__", r"\.qmlc", r"\.pyc"]
  22. # Re-use translation catalog.
  23. catalog = i18nCatalog("cura")
  24. def __init__(self, zip_file: bytes = None, meta_data: dict = None):
  25. self.zip_file = zip_file # type: Optional[bytes]
  26. self.meta_data = meta_data # type: Optional[dict]
  27. ## Create a back-up from the current user config folder.
  28. def makeFromCurrent(self) -> (bool, Optional[str]):
  29. cura_release = CuraApplication.getInstance().getVersion()
  30. version_data_dir = Resources.getDataStoragePath()
  31. Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir)
  32. # Ensure all current settings are saved.
  33. CuraApplication.getInstance().saveSettings()
  34. # We copy the preferences file to the user data directory in Linux as it's in a different location there.
  35. # When restoring a backup on Linux, we move it back.
  36. if Platform.isLinux():
  37. preferences_file_name = CuraApplication.getInstance().getApplicationName()
  38. preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name))
  39. backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))
  40. Logger.log("d", "Copying preferences file from %s to %s", preferences_file, backup_preferences_file)
  41. shutil.copyfile(preferences_file, backup_preferences_file)
  42. # Create an empty buffer and write the archive to it.
  43. buffer = io.BytesIO()
  44. archive = self._makeArchive(buffer, version_data_dir)
  45. files = archive.namelist()
  46. # Count the metadata items. We do this in a rather naive way at the moment.
  47. machine_count = len([s for s in files if "machine_instances/" in s]) - 1
  48. material_count = len([s for s in files if "materials/" in s]) - 1
  49. profile_count = len([s for s in files if "quality_changes/" in s]) - 1
  50. plugin_count = len([s for s in files if "plugin.json" in s])
  51. # Store the archive and metadata so the BackupManager can fetch them when needed.
  52. self.zip_file = buffer.getvalue()
  53. self.meta_data = {
  54. "cura_release": cura_release,
  55. "machine_count": str(machine_count),
  56. "material_count": str(material_count),
  57. "profile_count": str(profile_count),
  58. "plugin_count": str(plugin_count)
  59. }
  60. ## Make a full archive from the given root path with the given name.
  61. # \param root_path The root directory to archive recursively.
  62. # \return The archive as bytes.
  63. def _makeArchive(self, buffer: "io.BytesIO", root_path: str) -> Optional[ZipFile]:
  64. ignore_string = re.compile("|".join(self.IGNORED_FILES))
  65. try:
  66. archive = ZipFile(buffer, "w", ZIP_DEFLATED)
  67. for root, folders, files in os.walk(root_path):
  68. for item_name in folders + files:
  69. absolute_path = os.path.join(root, item_name)
  70. if ignore_string.search(absolute_path):
  71. continue
  72. archive.write(absolute_path, absolute_path[len(root_path) + len(os.sep):])
  73. archive.close()
  74. return archive
  75. except (IOError, OSError, BadZipfile) as error:
  76. Logger.log("e", "Could not create archive from user data directory: %s", error)
  77. self._showMessage(
  78. self.catalog.i18nc("@info:backup_failed",
  79. "Could not create archive from user data directory: {}".format(error)))
  80. return None
  81. ## Show a UI message.
  82. def _showMessage(self, message: str) -> None:
  83. Message(message, title=self.catalog.i18nc("@info:title", "Backup"), lifetime=30).show()
  84. ## Restore this back-up.
  85. # \return Whether we had success or not.
  86. def restore(self) -> bool:
  87. if not self.zip_file or not self.meta_data or not self.meta_data.get("cura_release", None):
  88. # We can restore without the minimum required information.
  89. Logger.log("w", "Tried to restore a Cura backup without having proper data or meta data.")
  90. self._showMessage(
  91. self.catalog.i18nc("@info:backup_failed",
  92. "Tried to restore a Cura backup without having proper data or meta data."))
  93. return False
  94. current_version = CuraApplication.getInstance().getVersion()
  95. version_to_restore = self.meta_data.get("cura_release", "master")
  96. if current_version != version_to_restore:
  97. # Cannot restore version older or newer than current because settings might have changed.
  98. # Restoring this will cause a lot of issues so we don't allow this for now.
  99. self._showMessage(
  100. self.catalog.i18nc("@info:backup_failed",
  101. "Tried to restore a Cura backup that does not match your current version."))
  102. return False
  103. version_data_dir = Resources.getDataStoragePath()
  104. archive = ZipFile(io.BytesIO(self.zip_file), "r")
  105. extracted = self._extractArchive(archive, version_data_dir)
  106. # Under Linux, preferences are stored elsewhere, so we copy the file to there.
  107. if Platform.isLinux():
  108. preferences_file_name = CuraApplication.getInstance().getApplicationName()
  109. preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name))
  110. backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))
  111. Logger.log("d", "Moving preferences file from %s to %s", backup_preferences_file, preferences_file)
  112. shutil.move(backup_preferences_file, preferences_file)
  113. return extracted
  114. ## Extract the whole archive to the given target path.
  115. # \param archive The archive as ZipFile.
  116. # \param target_path The target path.
  117. # \return Whether we had success or not.
  118. @staticmethod
  119. def _extractArchive(archive: "ZipFile", target_path: str) -> bool:
  120. Logger.log("d", "Removing current data in location: %s", target_path)
  121. Resources.factoryReset()
  122. Logger.log("d", "Extracting backup to location: %s", target_path)
  123. archive.extractall(target_path)
  124. return True