From 64819d517eb3ad980fe17948258c0da42a635903 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 7 May 2018 15:09:08 +0200 Subject: [PATCH] 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.