Backup.py 7.0 KB

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