From 32e2723c26ecbce61819fc087ade20754c0e5b97 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 7 May 2018 10:55:24 +0200 Subject: [PATCH 01/35] Start class layouts for backups and plugins API --- cura/Api/Backups.py | 27 +++++++++++++++++++++++++++ cura/Api/__init__.py | 15 +++++++++++++++ cura/Backups/Backup.py | 29 +++++++++++++++++++++++++++++ cura/Backups/BackupsManager.py | 28 ++++++++++++++++++++++++++++ 4 files changed, 99 insertions(+) create mode 100644 cura/Api/Backups.py create mode 100644 cura/Api/__init__.py create mode 100644 cura/Backups/Backup.py create mode 100644 cura/Backups/BackupsManager.py diff --git a/cura/Api/Backups.py b/cura/Api/Backups.py new file mode 100644 index 0000000000..a05c3c3e64 --- /dev/null +++ b/cura/Api/Backups.py @@ -0,0 +1,27 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +from zipfile import ZipFile + +from cura.Backups.BackupsManager import BackupsManager + + +class Backups: + """ + The backups API provides a version-proof bridge between Cura's BackupManager and plugins that hook into it. + + Usage: + cura.Api.backups.createBackup() + cura.Api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"}) + """ + + manager = BackupsManager() # Re-used instance of the backups manager. + + def createBackup(self) -> ("ZipFile", dict): + """ + Create a new backup using the BackupsManager. + :return: + """ + return self.manager.createBackup() + + def restoreBackup(self, zip_file: "ZipFile", meta_data: dict) -> None: + return self.manager.restoreBackup(zip_file, meta_data) diff --git a/cura/Api/__init__.py b/cura/Api/__init__.py new file mode 100644 index 0000000000..675e31cf2b --- /dev/null +++ b/cura/Api/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +from cura.Api.Backups import Backups + + +class CuraApi: + """ + The official Cura API that plugins can use to interact with Cura. + Python does not technically prevent talking to other classes as well, + but this API provides a version-safe interface with proper deprecation warning etc. + Usage of any other methods than the ones provided in this API can cause plugins to be unstable. + """ + + # Backups API. + backups = Backups() diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py new file mode 100644 index 0000000000..f16dadbfb9 --- /dev/null +++ b/cura/Backups/Backup.py @@ -0,0 +1,29 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + + +class Backup: + """ + The backup class holds all data about a backup. + It is also responsible for reading and writing the zip file to the user data folder. + """ + + def __init__(self): + self.generated = False # type: bool + self.backup_id = None # type: str + self.target_cura_version = None # type: str + self.zip_file = None + self.meta_data = None # type: dict + + def getZipFile(self): + pass + + def getMetaData(self): + pass + + def create(self): + self.generated = True + pass + + def restore(self): + pass diff --git a/cura/Backups/BackupsManager.py b/cura/Backups/BackupsManager.py new file mode 100644 index 0000000000..f98e0e4d36 --- /dev/null +++ b/cura/Backups/BackupsManager.py @@ -0,0 +1,28 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +from zipfile import ZipFile + + +class BackupsManager: + """ + The BackupsManager is responsible for managing the creating and restoring of backups. + Backups themselves are represented in a different class. + """ + + def __init__(self): + pass + + def createBackup(self) -> ("ZipFile", dict): + """ + Get a backup of the current configuration. + :return: A Tuple containing a ZipFile (the actual backup) and a dict containing some meta data (like version). + """ + pass + + def restoreBackup(self, zip_file: "ZipFile", meta_data: dict) -> None: + """ + Restore a backup from a given ZipFile. + :param zip_file: A ZipFile containing the actual backup. + :param meta_data: A dict containing some meta data that is needed to restore the backup correctly. + """ + pass From 64819d517eb3ad980fe17948258c0da42a635903 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 7 May 2018 15:09:08 +0200 Subject: [PATCH 02/35] Start implementing backups functionality --- cura/Api/Backups.py | 13 ++++-- cura/Api/__init__.py | 4 ++ cura/Backups/Backup.py | 83 ++++++++++++++++++++++++++++------ cura/Backups/BackupsManager.py | 33 ++++++++++++-- 4 files changed, 110 insertions(+), 23 deletions(-) diff --git a/cura/Api/Backups.py b/cura/Api/Backups.py index a05c3c3e64..aa4d7f9816 100644 --- a/cura/Api/Backups.py +++ b/cura/Api/Backups.py @@ -10,8 +10,10 @@ class Backups: The backups API provides a version-proof bridge between Cura's BackupManager and plugins that hook into it. Usage: - cura.Api.backups.createBackup() - cura.Api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"}) + from cura.Api import CuraApi + api = CuraApi() + api.backups.createBackup() + api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"}) """ manager = BackupsManager() # Re-used instance of the backups manager. @@ -19,9 +21,14 @@ class Backups: def createBackup(self) -> ("ZipFile", dict): """ Create a new backup using the BackupsManager. - :return: + :return: Tuple containing a ZIP file with the backup data and a dict with meta data about the backup. """ return self.manager.createBackup() def restoreBackup(self, zip_file: "ZipFile", meta_data: dict) -> None: + """ + Restore a backup using the BackupManager. + :param zip_file: A ZIP file containing the actual backup data. + :param meta_data: Some meta data needed for restoring a backup, like the Cura version number. + """ return self.manager.restoreBackup(zip_file, meta_data) diff --git a/cura/Api/__init__.py b/cura/Api/__init__.py index 675e31cf2b..4bc38a8297 100644 --- a/cura/Api/__init__.py +++ b/cura/Api/__init__.py @@ -1,5 +1,6 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from UM.PluginRegistry import PluginRegistry from cura.Api.Backups import Backups @@ -11,5 +12,8 @@ class CuraApi: Usage of any other methods than the ones provided in this API can cause plugins to be unstable. """ + # For now we use the same API version to be consistent. + VERSION = PluginRegistry.APIVersion + # Backups API. backups = Backups() diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index f16dadbfb9..35ee594ad6 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -1,5 +1,13 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +import os +from datetime import datetime +from typing import Optional +from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile + +from UM.Logger import Logger +from UM.Resources import Resources +from cura.CuraApplication import CuraApplication class Backup: @@ -8,22 +16,67 @@ class Backup: It is also responsible for reading and writing the zip file to the user data folder. """ - def __init__(self): - self.generated = False # type: bool - self.backup_id = None # type: str - self.target_cura_version = None # type: str - self.zip_file = None - self.meta_data = None # type: dict + def __init__(self, zip_file: "ZipFile" = None, meta_data: dict = None): + self.zip_file = zip_file # type: Optional[ZipFile] + self.meta_data = meta_data # type: Optional[dict - def getZipFile(self): - pass + def makeFromCurrent(self) -> (bool, Optional[str]): + """ + Create a backup from the current user config folder. + """ + cura_release = CuraApplication.getInstance().getVersion() + version_data_dir = Resources.getDataStoragePath() + timestamp = datetime.now().isoformat() - def getMetaData(self): - pass + Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir) - def create(self): - self.generated = True - pass + # We're using an easy to parse filename for when we're restoring edge cases: + # TIMESTAMP.backup.VERSION.cura.zip + archive = self._makeArchive("{}.backup.{}.cura.zip".format(timestamp, cura_release), version_data_dir) - def restore(self): - pass + self.zip_file = archive + self.meta_data = { + "cura_release": cura_release + } + # TODO: fill meta data with machine/material/etc counts. + + @staticmethod + def _makeArchive(root_path: str, archive_name: str) -> Optional[ZipFile]: + """ + Make a full archive from the given root path with the given name. + :param root_path: The root directory to archive recursively. + :param archive_name: The name of the archive to create. + :return: The archive as ZipFile. + """ + parent_folder = os.path.dirname(root_path) + contents = os.walk(root_path) + try: + archive = ZipFile(archive_name, "w", ZIP_DEFLATED) + for root, folders, files in contents: + for folder_name in folders: + # Add all folders, even empty ones. + absolute_path = os.path.join(root, folder_name) + relative_path = absolute_path.replace(parent_folder + '\\', '') + archive.write(absolute_path, relative_path) + for file_name in files: + # Add all files. + absolute_path = os.path.join(root, file_name) + relative_path = absolute_path.replace(parent_folder + '\\', '') + archive.write(absolute_path, relative_path) + archive.close() + return archive + except (IOError, OSError, BadZipfile) as error: + Logger.log("e", "Could not create archive from user data directory: %s", error) + return None + + def restore(self) -> None: + """ + Restore this backup. + """ + if not self.zip_file or not self.meta_data or not self.meta_data.get("cura_release", None): + # We can restore without the minimum required information. + Logger.log("w", "Tried to restore a Cura backup without having proper data or meta data.") + return + + # global_data_dir = os.path.dirname(version_data_dir) + # TODO: restore logic. diff --git a/cura/Backups/BackupsManager.py b/cura/Backups/BackupsManager.py index f98e0e4d36..e649c7ec1f 100644 --- a/cura/Backups/BackupsManager.py +++ b/cura/Backups/BackupsManager.py @@ -2,6 +2,9 @@ # Cura is released under the terms of the LGPLv3 or higher. from zipfile import ZipFile +from UM.Logger import Logger +from cura.Backups.Backup import Backup + class BackupsManager: """ @@ -9,15 +12,17 @@ class BackupsManager: Backups themselves are represented in a different class. """ - def __init__(self): - pass - def createBackup(self) -> ("ZipFile", dict): """ Get a backup of the current configuration. :return: A Tuple containing a ZipFile (the actual backup) and a dict containing some meta data (like version). """ - pass + self._disableAutoSave() + backup = Backup() + backup.makeFromCurrent() + self._enableAutoSave() + # We don't return a Backup here because we want plugins only to interact with our API and not full objects. + return backup.zip_file, backup.meta_data def restoreBackup(self, zip_file: "ZipFile", meta_data: dict) -> None: """ @@ -25,4 +30,22 @@ class BackupsManager: :param zip_file: A ZipFile containing the actual backup. :param meta_data: A dict containing some meta data that is needed to restore the backup correctly. """ - pass + if not meta_data.get("cura_release", None): + # If there is no "cura_release" specified in the meta data, we don't execute a backup restore. + Logger.log("w", "Tried to restore a backup without specifying a Cura version number.") + return + + # TODO: first make a new backup to prevent data loss when restoring fails. + + self._disableAutoSave() + + backup = Backup(zip_file = zip_file, meta_data = meta_data) + backup.restore() # At this point, Cura will need to restart for the changes to take effect + + def _disableAutoSave(self): + """Here we try to disable the auto-save plugin as it might interfere with restoring a backup.""" + # TODO: Disable auto-save if possible. + + def _enableAutoSave(self): + """Re-enable auto-save after we're done.""" + # TODO: Enable auto-save if possible. From 936de402ec16f1293fbd6865c69210657eb10980 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 7 May 2018 15:40:47 +0200 Subject: [PATCH 03/35] use bytes to pass backup file around, generate in memory, small fixes --- cura/Api/Backups.py | 6 ++---- cura/Backups/Backup.py | 25 ++++++++++++------------- cura/Backups/BackupsManager.py | 6 +++--- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/cura/Api/Backups.py b/cura/Api/Backups.py index aa4d7f9816..663715ec29 100644 --- a/cura/Api/Backups.py +++ b/cura/Api/Backups.py @@ -1,7 +1,5 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from zipfile import ZipFile - from cura.Backups.BackupsManager import BackupsManager @@ -18,14 +16,14 @@ class Backups: manager = BackupsManager() # Re-used instance of the backups manager. - def createBackup(self) -> ("ZipFile", dict): + def createBackup(self) -> (bytes, dict): """ Create a new backup using the BackupsManager. :return: Tuple containing a ZIP file with the backup data and a dict with meta data about the backup. """ return self.manager.createBackup() - def restoreBackup(self, zip_file: "ZipFile", meta_data: dict) -> None: + def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None: """ Restore a backup using the BackupManager. :param zip_file: A ZIP file containing the actual backup data. diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 35ee594ad6..6c20df2b2a 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -1,7 +1,7 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +import io import os -from datetime import datetime from typing import Optional from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile @@ -16,9 +16,9 @@ class Backup: It is also responsible for reading and writing the zip file to the user data folder. """ - def __init__(self, zip_file: "ZipFile" = None, meta_data: dict = None): - self.zip_file = zip_file # type: Optional[ZipFile] - self.meta_data = meta_data # type: Optional[dict + def __init__(self, zip_file: bytes = None, meta_data: dict = None): + self.zip_file = zip_file # type: Optional[bytes] + self.meta_data = meta_data # type: Optional[dict] def makeFromCurrent(self) -> (bool, Optional[str]): """ @@ -26,13 +26,12 @@ class Backup: """ cura_release = CuraApplication.getInstance().getVersion() version_data_dir = Resources.getDataStoragePath() - timestamp = datetime.now().isoformat() Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir) # We're using an easy to parse filename for when we're restoring edge cases: # TIMESTAMP.backup.VERSION.cura.zip - archive = self._makeArchive("{}.backup.{}.cura.zip".format(timestamp, cura_release), version_data_dir) + archive = self._makeArchive(version_data_dir) self.zip_file = archive self.meta_data = { @@ -41,30 +40,30 @@ class Backup: # TODO: fill meta data with machine/material/etc counts. @staticmethod - def _makeArchive(root_path: str, archive_name: str) -> Optional[ZipFile]: + def _makeArchive(root_path: str) -> Optional[bytes]: """ Make a full archive from the given root path with the given name. :param root_path: The root directory to archive recursively. - :param archive_name: The name of the archive to create. - :return: The archive as ZipFile. + :return: The archive as bytes. """ parent_folder = os.path.dirname(root_path) contents = os.walk(root_path) try: - archive = ZipFile(archive_name, "w", ZIP_DEFLATED) + buffer = io.BytesIO() + archive = ZipFile(buffer, "w", ZIP_DEFLATED) for root, folders, files in contents: for folder_name in folders: # Add all folders, even empty ones. absolute_path = os.path.join(root, folder_name) relative_path = absolute_path.replace(parent_folder + '\\', '') - archive.write(absolute_path, relative_path) + archive.write(relative_path) for file_name in files: # Add all files. absolute_path = os.path.join(root, file_name) relative_path = absolute_path.replace(parent_folder + '\\', '') - archive.write(absolute_path, relative_path) + archive.write(relative_path) archive.close() - return archive + return buffer.getvalue() except (IOError, OSError, BadZipfile) as error: Logger.log("e", "Could not create archive from user data directory: %s", error) return None diff --git a/cura/Backups/BackupsManager.py b/cura/Backups/BackupsManager.py index e649c7ec1f..e43a279ab6 100644 --- a/cura/Backups/BackupsManager.py +++ b/cura/Backups/BackupsManager.py @@ -1,6 +1,6 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from zipfile import ZipFile +from typing import Optional from UM.Logger import Logger from cura.Backups.Backup import Backup @@ -12,7 +12,7 @@ class BackupsManager: Backups themselves are represented in a different class. """ - def createBackup(self) -> ("ZipFile", dict): + def createBackup(self) -> (Optional[bytes], Optional[dict]): """ Get a backup of the current configuration. :return: A Tuple containing a ZipFile (the actual backup) and a dict containing some meta data (like version). @@ -24,7 +24,7 @@ class BackupsManager: # We don't return a Backup here because we want plugins only to interact with our API and not full objects. return backup.zip_file, backup.meta_data - def restoreBackup(self, zip_file: "ZipFile", meta_data: dict) -> None: + def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None: """ Restore a backup from a given ZipFile. :param zip_file: A ZipFile containing the actual backup. From ce0c14451f935ce3eb36128ab47a427b52dab08a Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 7 May 2018 17:31:10 +0200 Subject: [PATCH 04/35] Fix spelling in API docs --- cura/Api/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/Api/__init__.py b/cura/Api/__init__.py index 4bc38a8297..c7cb8b5201 100644 --- a/cura/Api/__init__.py +++ b/cura/Api/__init__.py @@ -8,7 +8,7 @@ class CuraApi: """ The official Cura API that plugins can use to interact with Cura. Python does not technically prevent talking to other classes as well, - but this API provides a version-safe interface with proper deprecation warning etc. + but this API provides a version-safe interface with proper deprecation warnings etc. Usage of any other methods than the ones provided in this API can cause plugins to be unstable. """ From a4882d8f83eb966171892e81af6ac909a4368f9c Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 8 May 2018 11:46:09 +0200 Subject: [PATCH 05/35] Fixes for archiving paths in backup, fake meta data --- cura/Backups/Backup.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 6c20df2b2a..3d3cf0be52 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -35,9 +35,13 @@ class Backup: self.zip_file = archive self.meta_data = { - "cura_release": cura_release + "cura_release": cura_release, + "machine_count": 0, + "material_count": 0, + "profile_count": 0, + "plugin_count": 0 } - # TODO: fill meta data with machine/material/etc counts. + # TODO: fill meta data with real machine/material/etc counts. @staticmethod def _makeArchive(root_path: str) -> Optional[bytes]: @@ -46,7 +50,6 @@ class Backup: :param root_path: The root directory to archive recursively. :return: The archive as bytes. """ - parent_folder = os.path.dirname(root_path) contents = os.walk(root_path) try: buffer = io.BytesIO() @@ -55,13 +58,13 @@ class Backup: for folder_name in folders: # Add all folders, even empty ones. absolute_path = os.path.join(root, folder_name) - relative_path = absolute_path.replace(parent_folder + '\\', '') - archive.write(relative_path) + relative_path = absolute_path[len(root_path) + len(os.sep):] + archive.write(absolute_path, relative_path) for file_name in files: # Add all files. absolute_path = os.path.join(root, file_name) - relative_path = absolute_path.replace(parent_folder + '\\', '') - archive.write(relative_path) + relative_path = absolute_path[len(root_path) + len(os.sep):] + archive.write(absolute_path, relative_path) archive.close() return buffer.getvalue() except (IOError, OSError, BadZipfile) as error: From 1b1d99c4bc9b88a96fb73bf452d2b8489f298082 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 8 May 2018 13:22:17 +0200 Subject: [PATCH 06/35] Ignore cura.log in backups --- cura/Backups/Backup.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 3d3cf0be52..50f4383c31 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -16,6 +16,9 @@ class Backup: It is also responsible for reading and writing the zip file to the user data folder. """ + # These files should be ignored when making a backup. + IGNORED_FILES = {"cura.log"} + def __init__(self, zip_file: bytes = None, meta_data: dict = None): self.zip_file = zip_file # type: Optional[bytes] self.meta_data = meta_data # type: Optional[dict] @@ -43,8 +46,7 @@ class Backup: } # TODO: fill meta data with real machine/material/etc counts. - @staticmethod - def _makeArchive(root_path: str) -> Optional[bytes]: + def _makeArchive(self, root_path: str) -> Optional[bytes]: """ Make a full archive from the given root path with the given name. :param root_path: The root directory to archive recursively. @@ -61,7 +63,9 @@ class Backup: relative_path = absolute_path[len(root_path) + len(os.sep):] archive.write(absolute_path, relative_path) for file_name in files: - # Add all files. + # Add all files except the ignored ones. + if file_name in self.IGNORED_FILES: + return absolute_path = os.path.join(root, file_name) relative_path = absolute_path[len(root_path) + len(os.sep):] archive.write(absolute_path, relative_path) From c827703417ad9d1bc29ddd07e87de0e7ffb0fdde Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 8 May 2018 13:47:02 +0200 Subject: [PATCH 07/35] Fix ignoring files --- cura/Backups/Backup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 50f4383c31..af9083ffd7 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -65,7 +65,7 @@ class Backup: for file_name in files: # Add all files except the ignored ones. if file_name in self.IGNORED_FILES: - return + continue absolute_path = os.path.join(root, file_name) relative_path = absolute_path[len(root_path) + len(os.sep):] archive.write(absolute_path, relative_path) From 703e52c0c7dfa3b23d64e31bbc64227d61c3c2de Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Wed, 9 May 2018 11:25:19 +0200 Subject: [PATCH 08/35] Ignore cura.cfg in the backups as it might contain secret data from plugins --- cura/Backups/Backup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index af9083ffd7..2bf945ac64 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -17,7 +17,8 @@ class Backup: """ # These files should be ignored when making a backup. - IGNORED_FILES = {"cura.log"} + # Cura.cfg might contain secret data, so we don't back it up for now. + IGNORED_FILES = {"cura.log", "cura.cfg"} def __init__(self, zip_file: bytes = None, meta_data: dict = None): self.zip_file = zip_file # type: Optional[bytes] From bc424509d9f2a346d0d47c33c203d0fc047e6817 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Wed, 9 May 2018 17:58:14 +0200 Subject: [PATCH 09/35] Fix docstring --- cura/Backups/BackupsManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/Backups/BackupsManager.py b/cura/Backups/BackupsManager.py index e43a279ab6..bb52ad57ba 100644 --- a/cura/Backups/BackupsManager.py +++ b/cura/Backups/BackupsManager.py @@ -27,7 +27,7 @@ class BackupsManager: def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None: """ Restore a backup from a given ZipFile. - :param zip_file: A ZipFile containing the actual backup. + :param zip_file: A bytes object containing the actual backup. :param meta_data: A dict containing some meta data that is needed to restore the backup correctly. """ if not meta_data.get("cura_release", None): From 79cebca9f0815c1d58939fe65da6ad99a9442c1e Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 11 May 2018 16:45:32 +0200 Subject: [PATCH 10/35] Rudimentary restore functionality --- cura/Backups/Backup.py | 28 +++++++++++++++++++++++----- cura/Backups/BackupsManager.py | 6 +++++- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 2bf945ac64..3cdcfa8e23 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -2,6 +2,8 @@ # Cura is released under the terms of the LGPLv3 or higher. import io import os +import shutil + from typing import Optional from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile @@ -17,8 +19,7 @@ class Backup: """ # These files should be ignored when making a backup. - # Cura.cfg might contain secret data, so we don't back it up for now. - IGNORED_FILES = {"cura.log", "cura.cfg"} + IGNORED_FILES = {"cura.log"} def __init__(self, zip_file: bytes = None, meta_data: dict = None): self.zip_file = zip_file # type: Optional[bytes] @@ -76,14 +77,31 @@ class Backup: Logger.log("e", "Could not create archive from user data directory: %s", error) return None - def restore(self) -> None: + def restore(self) -> bool: """ Restore this backup. """ if not self.zip_file or not self.meta_data or not self.meta_data.get("cura_release", None): # We can restore without the minimum required information. Logger.log("w", "Tried to restore a Cura backup without having proper data or meta data.") - return + return False # global_data_dir = os.path.dirname(version_data_dir) - # TODO: restore logic. + # TODO: handle restoring older data version. + + version_data_dir = Resources.getDataStoragePath() + archive = ZipFile(io.BytesIO(self.zip_file), "r") + extracted = self._extractArchive(archive, version_data_dir) + if not extracted: + return False + return True + + @staticmethod + def _extractArchive(archive: "ZipFile", target_path: str) -> bool: + Logger.log("d", "Removing current data in location: %s", target_path) + shutil.rmtree(target_path) + + Logger.log("d", "Extracting backup to location: %s", target_path) + archive.extractall(target_path) + + return True diff --git a/cura/Backups/BackupsManager.py b/cura/Backups/BackupsManager.py index bb52ad57ba..55430126dd 100644 --- a/cura/Backups/BackupsManager.py +++ b/cura/Backups/BackupsManager.py @@ -4,6 +4,7 @@ from typing import Optional from UM.Logger import Logger from cura.Backups.Backup import Backup +from cura.CuraApplication import CuraApplication class BackupsManager: @@ -40,7 +41,10 @@ class BackupsManager: self._disableAutoSave() backup = Backup(zip_file = zip_file, meta_data = meta_data) - backup.restore() # At this point, Cura will need to restart for the changes to take effect + restored = backup.restore() + if restored: + # At this point, Cura will need to restart for the changes to take effect. + CuraApplication.getInstance().windowClosed() def _disableAutoSave(self): """Here we try to disable the auto-save plugin as it might interfere with restoring a backup.""" From a7342f461992e8c63a35d575786fec5826b64a93 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 11 May 2018 17:13:40 +0200 Subject: [PATCH 11/35] Do not safe data after restoring backup --- cura/Backups/Backup.py | 3 +++ cura/Backups/BackupsManager.py | 3 ++- cura/CuraApplication.py | 6 +++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 3cdcfa8e23..866a6269bb 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -34,6 +34,9 @@ class Backup: Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir) + # Ensure all current settings are saved. + CuraApplication.getInstance().saveSettings() + # We're using an easy to parse filename for when we're restoring edge cases: # TIMESTAMP.backup.VERSION.cura.zip archive = self._makeArchive(version_data_dir) diff --git a/cura/Backups/BackupsManager.py b/cura/Backups/BackupsManager.py index 55430126dd..1f8c706eee 100644 --- a/cura/Backups/BackupsManager.py +++ b/cura/Backups/BackupsManager.py @@ -44,7 +44,8 @@ class BackupsManager: restored = backup.restore() if restored: # At this point, Cura will need to restart for the changes to take effect. - CuraApplication.getInstance().windowClosed() + # We don't want to store the data at this point as that would override the just-restored backup. + CuraApplication.getInstance().windowClosed(safe_data=False) def _disableAutoSave(self): """Here we try to disable the auto-save plugin as it might interfere with restoring a backup.""" diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index c8adfbc93b..d26d2ae3f3 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -497,10 +497,10 @@ class CuraApplication(QtApplication): ## Cura has multiple locations where instance containers need to be saved, so we need to handle this differently. # # Note that the AutoSave plugin also calls this method. - def saveSettings(self): - if not self.started: # Do not do saving during application start + def saveSettings(self, safe_data: bool = True): + if not self.started or not safe_data: + # Do not do saving during application start or when data should not be safed on quit. return - ContainerRegistry.getInstance().saveDirtyContainers() def saveStack(self, stack): From 0e0492327cb23fe83eeea846dfda1366f149b189 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 11 May 2018 18:15:50 +0200 Subject: [PATCH 12/35] Fix missing argument in application stopped signal callback --- plugins/USBPrinting/USBPrinterOutputDeviceManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py index 6375c92879..f586f80dd4 100644 --- a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py +++ b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py @@ -54,7 +54,7 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin): self._check_updates = True self._update_thread.start() - def stop(self): + def stop(self, store_data: bool = True): self._check_updates = False def _onConnectionStateChanged(self, serial_port): From 4429b5b5c13c7e048a90407fc72581d6c7ef5f52 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Sat, 12 May 2018 23:35:01 +0200 Subject: [PATCH 13/35] Count backup items for meta data, small fixes --- cura/Backups/Backup.py | 57 +++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 866a6269bb..e851d52ccd 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -19,7 +19,7 @@ class Backup: """ # These files should be ignored when making a backup. - IGNORED_FILES = {"cura.log"} + IGNORED_FILES = {"cura.log", "cache"} def __init__(self, zip_file: bytes = None, meta_data: dict = None): self.zip_file = zip_file # type: Optional[bytes] @@ -37,21 +37,28 @@ class Backup: # Ensure all current settings are saved. CuraApplication.getInstance().saveSettings() - # We're using an easy to parse filename for when we're restoring edge cases: - # TIMESTAMP.backup.VERSION.cura.zip - archive = self._makeArchive(version_data_dir) - - self.zip_file = archive + # Create an empty buffer and write the archive to it. + buffer = io.BytesIO() + archive = self._makeArchive(buffer, version_data_dir) + files = archive.namelist() + + # Count the metadata items. We do this in a rather naive way at the moment. + machine_count = len([s for s in files if "machine_instances/" in s]) - 1 + material_count = len([s for s in files if "materials/" in s]) - 1 + profile_count = len([s for s in files if "quality_changes/" in s]) - 1 + plugin_count = len([s for s in files if "plugin.json" in s]) + + # Store the archive and metadata so the BackupManager can fetch them when needed. + self.zip_file = buffer.getvalue() self.meta_data = { "cura_release": cura_release, - "machine_count": 0, - "material_count": 0, - "profile_count": 0, - "plugin_count": 0 + "machine_count": str(machine_count), + "material_count": str(material_count), + "profile_count": str(profile_count), + "plugin_count": str(plugin_count) } - # TODO: fill meta data with real machine/material/etc counts. - def _makeArchive(self, root_path: str) -> Optional[bytes]: + def _makeArchive(self, buffer: "io.BytesIO", root_path: str) -> Optional[ZipFile]: """ Make a full archive from the given root path with the given name. :param root_path: The root directory to archive recursively. @@ -59,52 +66,56 @@ class Backup: """ contents = os.walk(root_path) try: - buffer = io.BytesIO() archive = ZipFile(buffer, "w", ZIP_DEFLATED) for root, folders, files in contents: for folder_name in folders: - # Add all folders, even empty ones. + if folder_name in self.IGNORED_FILES: + continue absolute_path = os.path.join(root, folder_name) relative_path = absolute_path[len(root_path) + len(os.sep):] archive.write(absolute_path, relative_path) for file_name in files: - # Add all files except the ignored ones. if file_name in self.IGNORED_FILES: continue absolute_path = os.path.join(root, file_name) relative_path = absolute_path[len(root_path) + len(os.sep):] archive.write(absolute_path, relative_path) archive.close() - return buffer.getvalue() + return archive except (IOError, OSError, BadZipfile) as error: Logger.log("e", "Could not create archive from user data directory: %s", error) + # TODO: show message. return None def restore(self) -> bool: """ - Restore this backup. + Restore this backups + :return: A boolean whether we had success or not. """ if not self.zip_file or not self.meta_data or not self.meta_data.get("cura_release", None): # We can restore without the minimum required information. Logger.log("w", "Tried to restore a Cura backup without having proper data or meta data.") + # TODO: show message. return False - # global_data_dir = os.path.dirname(version_data_dir) # TODO: handle restoring older data version. + # global_data_dir = os.path.dirname(version_data_dir) version_data_dir = Resources.getDataStoragePath() archive = ZipFile(io.BytesIO(self.zip_file), "r") extracted = self._extractArchive(archive, version_data_dir) - if not extracted: - return False - return True + return extracted @staticmethod def _extractArchive(archive: "ZipFile", target_path: str) -> bool: + """ + Extract the whole archive to the given target path. + :param archive: The archive as ZipFile. + :param target_path: The target path. + :return: A boolean whether we had success or not. + """ Logger.log("d", "Removing current data in location: %s", target_path) shutil.rmtree(target_path) - Logger.log("d", "Extracting backup to location: %s", target_path) archive.extractall(target_path) - return True From 2742d61f9b401182afd7df2932923d7a753b0091 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Sun, 13 May 2018 01:20:42 +0200 Subject: [PATCH 14/35] Add TODO for later --- cura/Backups/Backup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index e851d52ccd..4c149b757a 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -33,6 +33,8 @@ class Backup: version_data_dir = Resources.getDataStoragePath() Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir) + + # TODO: support preferences file in backup under Linux (is in different directory). # Ensure all current settings are saved. CuraApplication.getInstance().saveSettings() @@ -99,6 +101,7 @@ class Backup: return False # TODO: handle restoring older data version. + # TODO: support preferences file in backup under Linux (is in different directory). # global_data_dir = os.path.dirname(version_data_dir) version_data_dir = Resources.getDataStoragePath() From c43007ca8e54c2aaa224fbf67090c344a18cb807 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Sun, 13 May 2018 15:47:35 +0200 Subject: [PATCH 15/35] Update git ignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d991fedb73..98eaa6f414 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ plugins/cura-siemensnx-plugin plugins/CuraBlenderPlugin plugins/CuraCloudPlugin plugins/CuraDrivePlugin +plugins/CuraDrive plugins/CuraLiveScriptingPlugin plugins/CuraOpenSCADPlugin plugins/CuraPrintProfileCreator From 30d66fb8de37cf252a5eb8fa13f19621110f93b2 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 14 May 2018 09:47:44 +0200 Subject: [PATCH 16/35] Copy preferences under Linux to add them to backup, notification messages --- cura/Backups/Backup.py | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 4c149b757a..375a1fa691 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -7,7 +7,11 @@ import shutil from typing import Optional from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile +from UM import i18nCatalog from UM.Logger import Logger +from UM.Message import Message +from UM.Platform import Platform +from UM.Preferences import Preferences from UM.Resources import Resources from cura.CuraApplication import CuraApplication @@ -21,6 +25,9 @@ class Backup: # These files should be ignored when making a backup. IGNORED_FILES = {"cura.log", "cache"} + # Re-use translation catalog. + catalog = i18nCatalog("cura") + def __init__(self, zip_file: bytes = None, meta_data: dict = None): self.zip_file = zip_file # type: Optional[bytes] self.meta_data = meta_data # type: Optional[dict] @@ -31,10 +38,15 @@ class Backup: """ cura_release = CuraApplication.getInstance().getVersion() version_data_dir = Resources.getDataStoragePath() + preferences_file_name = CuraApplication.getInstance().getApplicationName() + preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name)) Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir) - - # TODO: support preferences file in backup under Linux (is in different directory). + + # We copy the preferences file to the user data directory in Linux as it's in a different location there. + # When restoring a backup on Linux, we copy it back. + if Platform.isLinux(): + shutil.copyfile(preferences_file, os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))) # Ensure all current settings are saved. CuraApplication.getInstance().saveSettings() @@ -86,9 +98,15 @@ class Backup: return archive except (IOError, OSError, BadZipfile) as error: Logger.log("e", "Could not create archive from user data directory: %s", error) - # TODO: show message. + self._showMessage( + self.catalog.i18nc("@info:backup_failed", + "Could not create archive from user data directory: {}".format(error))) return None + def _showMessage(self, message: str) -> None: + """Show a UI message""" + Message(message, title=self.catalog.i18nc("@info:title", "Backup"), lifetime=30).show() + def restore(self) -> bool: """ Restore this backups @@ -97,16 +115,23 @@ class Backup: if not self.zip_file or not self.meta_data or not self.meta_data.get("cura_release", None): # We can restore without the minimum required information. Logger.log("w", "Tried to restore a Cura backup without having proper data or meta data.") - # TODO: show message. + self._showMessage( + self.catalog.i18nc("@info:backup_failed", + "Tried to restore a Cura backup without having proper data or meta data.")) return False # TODO: handle restoring older data version. - # TODO: support preferences file in backup under Linux (is in different directory). - # global_data_dir = os.path.dirname(version_data_dir) version_data_dir = Resources.getDataStoragePath() archive = ZipFile(io.BytesIO(self.zip_file), "r") extracted = self._extractArchive(archive, version_data_dir) + + # Under Linux, preferences are stored elsewhere, so we copy the file to there. + preferences_file_name = CuraApplication.getInstance().getApplicationName() + preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name)) + if Platform.isLinux(): + shutil.move(os.path.join(version_data_dir, "{}.cfg".format(preferences_file)), preferences_file) + return extracted @staticmethod From 2ed4b1b014c0aa5c0844022e23c74bb7bfcc45a6 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 14 May 2018 10:16:14 +0200 Subject: [PATCH 17/35] Fixes for restoring preferences on Linux --- cura/Backups/Backup.py | 18 +++++++++++------- cura/Backups/BackupsManager.py | 4 +--- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 375a1fa691..ffa0edc80f 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -38,15 +38,17 @@ class Backup: """ cura_release = CuraApplication.getInstance().getVersion() version_data_dir = Resources.getDataStoragePath() - preferences_file_name = CuraApplication.getInstance().getApplicationName() - preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name)) Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir) # We copy the preferences file to the user data directory in Linux as it's in a different location there. - # When restoring a backup on Linux, we copy it back. + # When restoring a backup on Linux, we move it back. if Platform.isLinux(): - shutil.copyfile(preferences_file, os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))) + preferences_file_name = CuraApplication.getInstance().getApplicationName() + preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name)) + backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name)) + Logger.log("d", "Copying preferences file from %s to %s", preferences_file, backup_preferences_file) + shutil.copyfile(preferences_file, backup_preferences_file) # Ensure all current settings are saved. CuraApplication.getInstance().saveSettings() @@ -127,10 +129,12 @@ class Backup: extracted = self._extractArchive(archive, version_data_dir) # Under Linux, preferences are stored elsewhere, so we copy the file to there. - preferences_file_name = CuraApplication.getInstance().getApplicationName() - preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name)) if Platform.isLinux(): - shutil.move(os.path.join(version_data_dir, "{}.cfg".format(preferences_file)), preferences_file) + preferences_file_name = CuraApplication.getInstance().getApplicationName() + preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name)) + backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name)) + Logger.log("d", "Moving preferences file from %s to %s", backup_preferences_file, preferences_file) + shutil.move(backup_preferences_file, preferences_file) return extracted diff --git a/cura/Backups/BackupsManager.py b/cura/Backups/BackupsManager.py index 1f8c706eee..04955692ee 100644 --- a/cura/Backups/BackupsManager.py +++ b/cura/Backups/BackupsManager.py @@ -36,8 +36,6 @@ class BackupsManager: Logger.log("w", "Tried to restore a backup without specifying a Cura version number.") return - # TODO: first make a new backup to prevent data loss when restoring fails. - self._disableAutoSave() backup = Backup(zip_file = zip_file, meta_data = meta_data) @@ -45,7 +43,7 @@ class BackupsManager: if restored: # At this point, Cura will need to restart for the changes to take effect. # We don't want to store the data at this point as that would override the just-restored backup. - CuraApplication.getInstance().windowClosed(safe_data=False) + CuraApplication.getInstance().windowClosed(save_data=False) def _disableAutoSave(self): """Here we try to disable the auto-save plugin as it might interfere with restoring a backup.""" From 0617a95cd0797e3739b54e0f89e73fc5e132fe93 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 15 May 2018 11:13:27 +0200 Subject: [PATCH 18/35] Add API root as cura version variable --- plugins/Toolbox/src/Toolbox.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index 2ba91dcdba..c5b7ababc5 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -24,17 +24,26 @@ from .PackagesModel import PackagesModel i18n_catalog = i18nCatalog("cura") + ## The Toolbox class is responsible of communicating with the server through the API class Toolbox(QObject, Extension): + + DEFAULT_PACKAGES_API_ROOT = "https://api.ultimaker.com" + def __init__(self, parent=None) -> None: super().__init__(parent) self._application = Application.getInstance() self._package_manager = None self._plugin_registry = Application.getInstance().getPluginRegistry() + self._packages_api_root = self._getPackagesApiRoot() self._packages_version = self._getPackagesVersion() self._api_version = 1 - self._api_url = "https://api.ultimaker.com/cura-packages/v{api_version}/cura/v{package_version}".format( api_version = self._api_version, package_version = self._packages_version) + self._api_url = "{api_root}/cura-packages/v{api_version}/cura/v{package_version}".format( + api_root = self._packages_api_root, + api_version = self._api_version, + package_version = self._packages_version + ) # Network: self._get_packages_request = None @@ -152,6 +161,15 @@ class Toolbox(QObject, Extension): def _onAppInitialized(self) -> None: self._package_manager = Application.getInstance().getCuraPackageManager() + # Get the API root for the packages API depending on Cura version settings. + def _getPackagesApiRoot(self) -> str: + if not hasattr(cura, "CuraVersion"): + return self.DEFAULT_PACKAGES_API_ROOT + if not hasattr(cura.CuraVersion, "CuraPackagesApiRoot"): + return self.DEFAULT_PACKAGES_API_ROOT + return cura.CuraVersion.CuraPackagesApiRoot + + # Get the packages version depending on Cura version settings. def _getPackagesVersion(self) -> int: if not hasattr(cura, "CuraVersion"): return self._plugin_registry.APIVersion From abcf7bd18773c4b2f558e6f8920cbcf1e6ac1344 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 15 May 2018 11:25:56 +0200 Subject: [PATCH 19/35] Show package version in installed tab --- plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml b/plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml index 6004832a57..b585a084b3 100644 --- a/plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml +++ b/plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml @@ -90,6 +90,16 @@ Item color: model.enabled ? UM.Theme.getColor("text") : UM.Theme.getColor("lining") linkColor: UM.Theme.getColor("text_link") } + + Label + { + text: model.version + width: parent.width + height: UM.Theme.getSize("toolbox_property_label").height + color: UM.Theme.getColor("text") + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + } } ToolboxInstalledTileActions { From c9145c666145a9a7915db39a8aa01647ae4ad25c Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 15 May 2018 13:28:33 +0200 Subject: [PATCH 20/35] Small code improvements --- .../Toolbox/resources/qml/ToolboxInstalledTileActions.qml | 6 ++---- plugins/Toolbox/src/Toolbox.py | 6 +++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml b/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml index 5bbed2351c..3a1b9ba3d9 100644 --- a/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml +++ b/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml @@ -18,13 +18,11 @@ Column readyLabel: catalog.i18nc("@action:button", "Update") activeLabel: catalog.i18nc("@action:button", "Updating") completeLabel: catalog.i18nc("@action:button", "Updated") - readyAction: function() { + readyAction: { toolbox.activePackage = model toolbox.update(model.id) } - activeAction: function() { - toolbox.cancelDownload() - } + activeAction: toolbox.cancelDownload() // Don't allow installing while another download is running enabled: !(toolbox.isDownloading && toolbox.activePackage != model) opacity: enabled ? 1.0 : 0.5 diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index cb5272446d..fb74fefdfa 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -313,9 +313,9 @@ class Toolbox(QObject, Extension): if remote_package is None: return False - local_version = local_package["package_version"] - remote_version = remote_package["package_version"] - return Version(remote_version) > Version(local_version) + local_version = Version(local_package["package_version"]) + remote_version = Version(remote_package["package_version"]) + return remote_version > local_version @pyqtSlot(str, result = bool) def isInstalled(self, package_id: str) -> bool: From 21889130b45e44b79bfa749b13107eeed673115f Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Wed, 16 May 2018 19:25:19 +0200 Subject: [PATCH 21/35] Code cleanup --- cura/Stages/CuraStage.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cura/Stages/CuraStage.py b/cura/Stages/CuraStage.py index 8b7822ed7a..b2f6d61799 100644 --- a/cura/Stages/CuraStage.py +++ b/cura/Stages/CuraStage.py @@ -1,9 +1,10 @@ # Copyright (c) 2017 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from PyQt5.QtCore import pyqtProperty, QUrl, QObject +from PyQt5.QtCore import pyqtProperty, QUrl from UM.Stage import Stage + class CuraStage(Stage): def __init__(self, parent = None): From eb436a8b0e1ee259c5871fef57f6d19f60f811ad Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Thu, 17 May 2018 09:13:55 +0200 Subject: [PATCH 22/35] Ignore permission error on Windows when trying to remove log file --- cura/Backups/Backup.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index ffa0edc80f..615fb45297 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -147,7 +147,13 @@ class Backup: :return: A boolean whether we had success or not. """ Logger.log("d", "Removing current data in location: %s", target_path) - shutil.rmtree(target_path) + try: + shutil.rmtree(target_path) + except PermissionError as error: + # This happens if a file is already opened by another program, usually only the log file. + # For now we just ignore this as it doesn't harm the restore process. + Logger.log("w", "Permission error while trying to remove tree: %s", error) + pass Logger.log("d", "Extracting backup to location: %s", target_path) archive.extractall(target_path) return True From fb40ea13cc947f9f3d959e1d8c60ab46291eb834 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Thu, 17 May 2018 09:25:03 +0200 Subject: [PATCH 23/35] Try onerror --- cura/Backups/Backup.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 615fb45297..aeffcf80eb 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -138,8 +138,7 @@ class Backup: return extracted - @staticmethod - def _extractArchive(archive: "ZipFile", target_path: str) -> bool: + def _extractArchive(self, archive: "ZipFile", target_path: str) -> bool: """ Extract the whole archive to the given target path. :param archive: The archive as ZipFile. @@ -148,7 +147,7 @@ class Backup: """ Logger.log("d", "Removing current data in location: %s", target_path) try: - shutil.rmtree(target_path) + shutil.rmtree(target_path, onerror=self._onRemoveError) except PermissionError as error: # This happens if a file is already opened by another program, usually only the log file. # For now we just ignore this as it doesn't harm the restore process. @@ -157,3 +156,10 @@ class Backup: Logger.log("d", "Extracting backup to location: %s", target_path) archive.extractall(target_path) return True + + @staticmethod + def _onRemoveError(*args): + import stat # needed for file stat + func, path, _ = args # onerror returns a tuple containing function, path and exception info + os.chmod(path, stat.S_IWRITE) + os.remove(path) From f00459e4cc10cfa1b351b2e15cae1f1eec6305bb Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Thu, 17 May 2018 09:32:18 +0200 Subject: [PATCH 24/35] revert --- cura/Backups/Backup.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index aeffcf80eb..615fb45297 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -138,7 +138,8 @@ class Backup: return extracted - def _extractArchive(self, archive: "ZipFile", target_path: str) -> bool: + @staticmethod + def _extractArchive(archive: "ZipFile", target_path: str) -> bool: """ Extract the whole archive to the given target path. :param archive: The archive as ZipFile. @@ -147,7 +148,7 @@ class Backup: """ Logger.log("d", "Removing current data in location: %s", target_path) try: - shutil.rmtree(target_path, onerror=self._onRemoveError) + shutil.rmtree(target_path) except PermissionError as error: # This happens if a file is already opened by another program, usually only the log file. # For now we just ignore this as it doesn't harm the restore process. @@ -156,10 +157,3 @@ class Backup: Logger.log("d", "Extracting backup to location: %s", target_path) archive.extractall(target_path) return True - - @staticmethod - def _onRemoveError(*args): - import stat # needed for file stat - func, path, _ = args # onerror returns a tuple containing function, path and exception info - os.chmod(path, stat.S_IWRITE) - os.remove(path) From 8b0346e11be42637965802f09345cea4126b71f7 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Thu, 17 May 2018 10:15:11 +0200 Subject: [PATCH 25/35] For now just ignore locked files on windows --- cura/Backups/Backup.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 615fb45297..c2e795c783 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -138,8 +138,7 @@ class Backup: return extracted - @staticmethod - def _extractArchive(archive: "ZipFile", target_path: str) -> bool: + def _extractArchive(self, archive: "ZipFile", target_path: str) -> bool: """ Extract the whole archive to the given target path. :param archive: The archive as ZipFile. @@ -148,7 +147,7 @@ class Backup: """ Logger.log("d", "Removing current data in location: %s", target_path) try: - shutil.rmtree(target_path) + shutil.rmtree(target_path, ignore_errors=True, onerror=self._handleRemovalError) except PermissionError as error: # This happens if a file is already opened by another program, usually only the log file. # For now we just ignore this as it doesn't harm the restore process. @@ -157,3 +156,8 @@ class Backup: Logger.log("d", "Extracting backup to location: %s", target_path) archive.extractall(target_path) return True + + @staticmethod + def _handleRemovalError(*args): + func, path, _ = args + Logger.log("w", "Could not remove path %s when doing recursive delete, ignoring...", path) From 9410f93dbfae21b3739bb085e7ffeb8ae70d26c4 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Thu, 17 May 2018 10:16:13 +0200 Subject: [PATCH 26/35] Remove unused import --- cura/Backups/Backup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index c2e795c783..8bd1bdaeaa 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -11,7 +11,6 @@ from UM import i18nCatalog from UM.Logger import Logger from UM.Message import Message from UM.Platform import Platform -from UM.Preferences import Preferences from UM.Resources import Resources from cura.CuraApplication import CuraApplication From aa07de45ed90a739aa784b99cdbe20810fc8be7a Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Thu, 17 May 2018 12:13:01 +0200 Subject: [PATCH 27/35] Start fixing restore on Windows --- cura/Backups/Backup.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 8bd1bdaeaa..9cdd1ab117 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -1,5 +1,6 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +import fnmatch import io import os import shutil @@ -22,7 +23,7 @@ class Backup: """ # These files should be ignored when making a backup. - IGNORED_FILES = {"cura.log", "cache"} + IGNORED_FILES = {"cura.log", "cache", "(?s).qmlc"} # Re-use translation catalog. catalog = i18nCatalog("cura") @@ -84,14 +85,17 @@ class Backup: archive = ZipFile(buffer, "w", ZIP_DEFLATED) for root, folders, files in contents: for folder_name in folders: - if folder_name in self.IGNORED_FILES: - continue + for ignore_rule in self.IGNORED_FILES: + if fnmatch.fnmatch(ignore_rule, folder_name): + continue absolute_path = os.path.join(root, folder_name) relative_path = absolute_path[len(root_path) + len(os.sep):] archive.write(absolute_path, relative_path) for file_name in files: - if file_name in self.IGNORED_FILES: - continue + for ignore_rule in self.IGNORED_FILES: + if fnmatch.fnmatch(ignore_rule, file_name): + print("FNMATCH=====", ignore_rule, file_name) + continue absolute_path = os.path.join(root, file_name) relative_path = absolute_path[len(root_path) + len(os.sep):] archive.write(absolute_path, relative_path) @@ -145,18 +149,7 @@ class Backup: :return: A boolean whether we had success or not. """ Logger.log("d", "Removing current data in location: %s", target_path) - try: - shutil.rmtree(target_path, ignore_errors=True, onerror=self._handleRemovalError) - except PermissionError as error: - # This happens if a file is already opened by another program, usually only the log file. - # For now we just ignore this as it doesn't harm the restore process. - Logger.log("w", "Permission error while trying to remove tree: %s", error) - pass + Resources.factoryReset() Logger.log("d", "Extracting backup to location: %s", target_path) archive.extractall(target_path) return True - - @staticmethod - def _handleRemovalError(*args): - func, path, _ = args - Logger.log("w", "Could not remove path %s when doing recursive delete, ignoring...", path) From 7b3f334678309f0c9db4533f75fc781912fe0c55 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Thu, 17 May 2018 15:07:28 +0200 Subject: [PATCH 28/35] Fix ignored files for Windows --- cura/Backups/Backup.py | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 9cdd1ab117..5beefb0a1f 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -1,8 +1,9 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -import fnmatch import io import os +import re + import shutil from typing import Optional @@ -23,7 +24,7 @@ class Backup: """ # These files should be ignored when making a backup. - IGNORED_FILES = {"cura.log", "cache", "(?s).qmlc"} + IGNORED_FILES = [r"cura\.log", r"plugins\.json", r"cache", r"__pycache__", r"\.qmlc", r"\.pyc"] # Re-use translation catalog. catalog = i18nCatalog("cura") @@ -80,25 +81,15 @@ class Backup: :param root_path: The root directory to archive recursively. :return: The archive as bytes. """ - contents = os.walk(root_path) + ignore_string = re.compile("|".join(self.IGNORED_FILES)) try: archive = ZipFile(buffer, "w", ZIP_DEFLATED) - for root, folders, files in contents: - for folder_name in folders: - for ignore_rule in self.IGNORED_FILES: - if fnmatch.fnmatch(ignore_rule, folder_name): - continue - absolute_path = os.path.join(root, folder_name) - relative_path = absolute_path[len(root_path) + len(os.sep):] - archive.write(absolute_path, relative_path) - for file_name in files: - for ignore_rule in self.IGNORED_FILES: - if fnmatch.fnmatch(ignore_rule, file_name): - print("FNMATCH=====", ignore_rule, file_name) - continue - absolute_path = os.path.join(root, file_name) - relative_path = absolute_path[len(root_path) + len(os.sep):] - archive.write(absolute_path, relative_path) + for root, folders, files in os.walk(root_path): + for item_name in folders + files: + absolute_path = os.path.join(root, item_name) + if ignore_string.search(absolute_path): + continue + archive.write(absolute_path, absolute_path[len(root_path) + len(os.sep):]) archive.close() return archive except (IOError, OSError, BadZipfile) as error: @@ -141,7 +132,8 @@ class Backup: return extracted - def _extractArchive(self, archive: "ZipFile", target_path: str) -> bool: + @staticmethod + def _extractArchive(archive: "ZipFile", target_path: str) -> bool: """ Extract the whole archive to the given target path. :param archive: The archive as ZipFile. From a0d3dae92057d745508ec8d8c9c869f2b7532182 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 18 May 2018 10:07:20 +0200 Subject: [PATCH 29/35] Do not allow restore different version --- cura/Backups/Backup.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 5beefb0a1f..69718dd7c9 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -14,6 +14,7 @@ from UM.Logger import Logger from UM.Message import Message from UM.Platform import Platform from UM.Resources import Resources +from UM.Version import Version from cura.CuraApplication import CuraApplication @@ -116,7 +117,12 @@ class Backup: "Tried to restore a Cura backup without having proper data or meta data.")) return False - # TODO: handle restoring older data version. + current_version = CuraApplication.getInstance().getVersion() + version_to_restore = self.meta_data.get("cura_release", "master") + if current_version != version_to_restore: + # Cannot restore version older or newer than current because settings might have changed. + # Restoring this will cause a lot of issues so we don't allow this for now. + return False version_data_dir = Resources.getDataStoragePath() archive = ZipFile(io.BytesIO(self.zip_file), "r") From a20de3581a1d1855d2e1b9588db1be15dfa85f05 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 18 May 2018 10:08:00 +0200 Subject: [PATCH 30/35] Add info message when failed because of version mismatch --- cura/Backups/Backup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 69718dd7c9..65d8f184ec 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -122,6 +122,9 @@ class Backup: if current_version != version_to_restore: # Cannot restore version older or newer than current because settings might have changed. # Restoring this will cause a lot of issues so we don't allow this for now. + self._showMessage( + self.catalog.i18nc("@info:backup_failed", + "Tried to restore a Cura backup that does not match your current version.")) return False version_data_dir = Resources.getDataStoragePath() From 2fab1aef33cbbaff18244f3c67beee0424140d6e Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 18 May 2018 17:28:54 +0200 Subject: [PATCH 31/35] Rename Api to API --- cura/Api/Backups.py | 32 -------------------------------- cura/Api/__init__.py | 19 ------------------- 2 files changed, 51 deletions(-) delete mode 100644 cura/Api/Backups.py delete mode 100644 cura/Api/__init__.py diff --git a/cura/Api/Backups.py b/cura/Api/Backups.py deleted file mode 100644 index 663715ec29..0000000000 --- a/cura/Api/Backups.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (c) 2018 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. -from cura.Backups.BackupsManager import BackupsManager - - -class Backups: - """ - The backups API provides a version-proof bridge between Cura's BackupManager and plugins that hook into it. - - Usage: - from cura.Api import CuraApi - api = CuraApi() - api.backups.createBackup() - api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"}) - """ - - manager = BackupsManager() # Re-used instance of the backups manager. - - def createBackup(self) -> (bytes, dict): - """ - Create a new backup using the BackupsManager. - :return: Tuple containing a ZIP file with the backup data and a dict with meta data about the backup. - """ - return self.manager.createBackup() - - def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None: - """ - Restore a backup using the BackupManager. - :param zip_file: A ZIP file containing the actual backup data. - :param meta_data: Some meta data needed for restoring a backup, like the Cura version number. - """ - return self.manager.restoreBackup(zip_file, meta_data) diff --git a/cura/Api/__init__.py b/cura/Api/__init__.py deleted file mode 100644 index c7cb8b5201..0000000000 --- a/cura/Api/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2018 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. -from UM.PluginRegistry import PluginRegistry -from cura.Api.Backups import Backups - - -class CuraApi: - """ - The official Cura API that plugins can use to interact with Cura. - Python does not technically prevent talking to other classes as well, - but this API provides a version-safe interface with proper deprecation warnings etc. - Usage of any other methods than the ones provided in this API can cause plugins to be unstable. - """ - - # For now we use the same API version to be consistent. - VERSION = PluginRegistry.APIVersion - - # Backups API. - backups = Backups() From 5b5a8f77b7065caaafbd8c04878138e422a49fc9 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 18 May 2018 17:29:01 +0200 Subject: [PATCH 32/35] Rename Api to API --- cura/API/Backups.py | 32 ++++++++++++++++++++++++++++++++ cura/API/__init__.py | 19 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 cura/API/Backups.py create mode 100644 cura/API/__init__.py diff --git a/cura/API/Backups.py b/cura/API/Backups.py new file mode 100644 index 0000000000..ba416bd870 --- /dev/null +++ b/cura/API/Backups.py @@ -0,0 +1,32 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +from cura.Backups.BackupsManager import BackupsManager + + +class Backups: + """ + The backups API provides a version-proof bridge between Cura's BackupManager and plugins that hook into it. + + Usage: + from cura.API import CuraAPI + api = CuraAPI() + api.backups.createBackup() + api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"}) + """ + + manager = BackupsManager() # Re-used instance of the backups manager. + + def createBackup(self) -> (bytes, dict): + """ + Create a new backup using the BackupsManager. + :return: Tuple containing a ZIP file with the backup data and a dict with meta data about the backup. + """ + return self.manager.createBackup() + + def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None: + """ + Restore a backup using the BackupManager. + :param zip_file: A ZIP file containing the actual backup data. + :param meta_data: Some meta data needed for restoring a backup, like the Cura version number. + """ + return self.manager.restoreBackup(zip_file, meta_data) diff --git a/cura/API/__init__.py b/cura/API/__init__.py new file mode 100644 index 0000000000..7dd5d8f79e --- /dev/null +++ b/cura/API/__init__.py @@ -0,0 +1,19 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +from UM.PluginRegistry import PluginRegistry +from cura.API.Backups import Backups + + +class CuraAPI: + """ + The official Cura API that plugins can use to interact with Cura. + Python does not technically prevent talking to other classes as well, + but this API provides a version-safe interface with proper deprecation warnings etc. + Usage of any other methods than the ones provided in this API can cause plugins to be unstable. + """ + + # For now we use the same API version to be consistent. + VERSION = PluginRegistry.APIVersion + + # Backups API. + backups = Backups() From 2bbcb2dd7d80cb10369fdcc1255d5aa0c79bd550 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 22 May 2018 17:45:30 +0200 Subject: [PATCH 33/35] Save settings before moving config file on Linux --- cura/Backups/Backup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 65d8f184ec..89a3a54b59 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -43,6 +43,9 @@ class Backup: Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir) + # Ensure all current settings are saved. + CuraApplication.getInstance().saveSettings() + # We copy the preferences file to the user data directory in Linux as it's in a different location there. # When restoring a backup on Linux, we move it back. if Platform.isLinux(): @@ -52,9 +55,6 @@ class Backup: Logger.log("d", "Copying preferences file from %s to %s", preferences_file, backup_preferences_file) shutil.copyfile(preferences_file, backup_preferences_file) - # Ensure all current settings are saved. - CuraApplication.getInstance().saveSettings() - # Create an empty buffer and write the archive to it. buffer = io.BytesIO() archive = self._makeArchive(buffer, version_data_dir) From 92fa725ad60923d14771c5b7a06d2e89f9144d02 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Wed, 23 May 2018 10:00:19 +0200 Subject: [PATCH 34/35] Application.saveSettings() should save everything --- cura/CuraApplication.py | 3 +++ plugins/AutoSave/AutoSave.py | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 2e7eddd8fc..d1c7950884 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -505,6 +505,9 @@ class CuraApplication(QtApplication): return ContainerRegistry.getInstance().saveDirtyContainers() + Preferences.getInstance().writeToFile(Resources.getStoragePath(Resources.Preferences, + self._application_name + ".cfg")) + def saveStack(self, stack): ContainerRegistry.getInstance().saveContainer(stack) diff --git a/plugins/AutoSave/AutoSave.py b/plugins/AutoSave/AutoSave.py index 5fdac502b5..792e41ffd0 100644 --- a/plugins/AutoSave/AutoSave.py +++ b/plugins/AutoSave/AutoSave.py @@ -72,6 +72,4 @@ class AutoSave(Extension): Application.getInstance().saveSettings() - Preferences.getInstance().writeToFile(Resources.getStoragePath(Resources.Preferences, Application.getInstance().getApplicationName() + ".cfg")) - self._saving = False From cfd1b7b813ccdac76c623243a03633b2059d9a0e Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Wed, 23 May 2018 11:24:33 +0200 Subject: [PATCH 35/35] Fix AutoSave conflicts with BackupManager - Move AutoSave out of the plugins. It's a built-in module now. - Add enable/disable saving data on CuraApplication. - Avoid saving data in backup restore --- {plugins/AutoSave => cura}/AutoSave.py | 34 +++++--------------------- cura/Backups/BackupsManager.py | 2 ++ cura/CuraApplication.py | 16 ++++++++++-- plugins/AutoSave/__init__.py | 13 ---------- plugins/AutoSave/plugin.json | 8 ------ 5 files changed, 22 insertions(+), 51 deletions(-) rename {plugins/AutoSave => cura}/AutoSave.py (56%) delete mode 100644 plugins/AutoSave/__init__.py delete mode 100644 plugins/AutoSave/plugin.json diff --git a/plugins/AutoSave/AutoSave.py b/cura/AutoSave.py similarity index 56% rename from plugins/AutoSave/AutoSave.py rename to cura/AutoSave.py index 792e41ffd0..71e889a62b 100644 --- a/plugins/AutoSave/AutoSave.py +++ b/cura/AutoSave.py @@ -3,17 +3,13 @@ from PyQt5.QtCore import QTimer -from UM.Extension import Extension from UM.Preferences import Preferences -from UM.Application import Application -from UM.Resources import Resources from UM.Logger import Logger -class AutoSave(Extension): - def __init__(self): - super().__init__() - +class AutoSave: + def __init__(self, application): + self._application = application Preferences.getInstance().preferenceChanged.connect(self._triggerTimer) self._global_stack = None @@ -26,29 +22,11 @@ class AutoSave(Extension): self._saving = False - # At this point, the Application instance has not finished its constructor call yet, so directly using something - # like Application.getInstance() is not correct. The initialisation now will only gets triggered after the - # application finishes its start up successfully. - self._init_timer = QTimer() - self._init_timer.setInterval(1000) - self._init_timer.setSingleShot(True) - self._init_timer.timeout.connect(self.initialize) - self._init_timer.start() - def initialize(self): # only initialise if the application is created and has started - from cura.CuraApplication import CuraApplication - if not CuraApplication.Created: - self._init_timer.start() - return - if not CuraApplication.getInstance().started: - self._init_timer.start() - return - self._change_timer.timeout.connect(self._onTimeout) - Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged) + self._application.globalContainerStackChanged.connect(self._onGlobalStackChanged) self._onGlobalStackChanged() - self._triggerTimer() def _triggerTimer(self, *args): @@ -60,7 +38,7 @@ class AutoSave(Extension): self._global_stack.propertyChanged.disconnect(self._triggerTimer) self._global_stack.containersChanged.disconnect(self._triggerTimer) - self._global_stack = Application.getInstance().getGlobalContainerStack() + self._global_stack = self._application.getGlobalContainerStack() if self._global_stack: self._global_stack.propertyChanged.connect(self._triggerTimer) @@ -70,6 +48,6 @@ class AutoSave(Extension): self._saving = True # To prevent the save process from triggering another autosave. Logger.log("d", "Autosaving preferences, instances and profiles") - Application.getInstance().saveSettings() + self._application.saveSettings() self._saving = False diff --git a/cura/Backups/BackupsManager.py b/cura/Backups/BackupsManager.py index 04955692ee..38ffcac92b 100644 --- a/cura/Backups/BackupsManager.py +++ b/cura/Backups/BackupsManager.py @@ -48,7 +48,9 @@ class BackupsManager: def _disableAutoSave(self): """Here we try to disable the auto-save plugin as it might interfere with restoring a backup.""" # TODO: Disable auto-save if possible. + CuraApplication.getInstance().setSaveDataEnabled(False) def _enableAutoSave(self): """Re-enable auto-save after we're done.""" # TODO: Enable auto-save if possible. + CuraApplication.getInstance().setSaveDataEnabled(True) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index d1c7950884..5b191c9180 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -74,6 +74,7 @@ from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager from cura.Machines.VariantManager import VariantManager +from .AutoSave import AutoSave from . import PlatformPhysics from . import BuildVolume from . import CameraAnimation @@ -234,6 +235,8 @@ class CuraApplication(QtApplication): self._simple_mode_settings_manager = None self._cura_scene_controller = None self._machine_error_checker = None + self._auto_save = None + self._save_data_enabled = True self._additional_components = {} # Components to add to certain areas in the interface @@ -496,11 +499,14 @@ class CuraApplication(QtApplication): showPrintMonitor = pyqtSignal(bool, arguments = ["show"]) + def setSaveDataEnabled(self, enabled: bool) -> None: + self._save_data_enabled = enabled + ## Cura has multiple locations where instance containers need to be saved, so we need to handle this differently. # # Note that the AutoSave plugin also calls this method. - def saveSettings(self, safe_data: bool = True): - if not self.started or not safe_data: + def saveSettings(self): + if not self.started or not self._save_data_enabled: # Do not do saving during application start or when data should not be safed on quit. return ContainerRegistry.getInstance().saveDirtyContainers() @@ -728,6 +734,9 @@ class CuraApplication(QtApplication): self._post_start_timer.timeout.connect(self._onPostStart) self._post_start_timer.start() + self._auto_save = AutoSave(self) + self._auto_save.initialize() + self.exec_() def _onPostStart(self): @@ -879,6 +888,9 @@ class CuraApplication(QtApplication): return super().event(event) + def getAutoSave(self): + return self._auto_save + ## Get print information (duration / material used) def getPrintInformation(self): return self._print_information diff --git a/plugins/AutoSave/__init__.py b/plugins/AutoSave/__init__.py deleted file mode 100644 index d7ee0736a2..0000000000 --- a/plugins/AutoSave/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) 2016 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -from . import AutoSave - -from UM.i18n import i18nCatalog -catalog = i18nCatalog("cura") - -def getMetaData(): - return {} - -def register(app): - return { "extension": AutoSave.AutoSave() } diff --git a/plugins/AutoSave/plugin.json b/plugins/AutoSave/plugin.json deleted file mode 100644 index 32e07a1062..0000000000 --- a/plugins/AutoSave/plugin.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "Auto Save", - "author": "Ultimaker B.V.", - "version": "1.0.0", - "description": "Automatically saves Preferences, Machines and Profiles after changes.", - "api": 4, - "i18n-catalog": "cura" -}