Start implementing backups functionality

This commit is contained in:
ChrisTerBeke 2018-05-07 15:09:08 +02:00
parent 32e2723c26
commit 64819d517e
4 changed files with 110 additions and 23 deletions

View File

@ -10,8 +10,10 @@ class Backups:
The backups API provides a version-proof bridge between Cura's BackupManager and plugins that hook into it. The backups API provides a version-proof bridge between Cura's BackupManager and plugins that hook into it.
Usage: Usage:
cura.Api.backups.createBackup() from cura.Api import CuraApi
cura.Api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"}) api = CuraApi()
api.backups.createBackup()
api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})
""" """
manager = BackupsManager() # Re-used instance of the backups manager. manager = BackupsManager() # Re-used instance of the backups manager.
@ -19,9 +21,14 @@ class Backups:
def createBackup(self) -> ("ZipFile", dict): def createBackup(self) -> ("ZipFile", dict):
""" """
Create a new backup using the BackupsManager. 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() return self.manager.createBackup()
def restoreBackup(self, zip_file: "ZipFile", meta_data: dict) -> None: 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) return self.manager.restoreBackup(zip_file, meta_data)

View File

@ -1,5 +1,6 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.PluginRegistry import PluginRegistry
from cura.Api.Backups import Backups 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. 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 API.
backups = Backups() backups = Backups()

View File

@ -1,5 +1,13 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # 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: class Backup:
@ -8,22 +16,67 @@ class Backup:
It is also responsible for reading and writing the zip file to the user data folder. It is also responsible for reading and writing the zip file to the user data folder.
""" """
def __init__(self): def __init__(self, zip_file: "ZipFile" = None, meta_data: dict = None):
self.generated = False # type: bool self.zip_file = zip_file # type: Optional[ZipFile]
self.backup_id = None # type: str self.meta_data = meta_data # type: Optional[dict
self.target_cura_version = None # type: str
self.zip_file = None
self.meta_data = None # type: dict
def getZipFile(self): def makeFromCurrent(self) -> (bool, Optional[str]):
pass """
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): Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir)
pass
def create(self): # We're using an easy to parse filename for when we're restoring edge cases:
self.generated = True # TIMESTAMP.backup.VERSION.cura.zip
pass archive = self._makeArchive("{}.backup.{}.cura.zip".format(timestamp, cura_release), version_data_dir)
def restore(self): self.zip_file = archive
pass 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.

View File

@ -2,6 +2,9 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from zipfile import ZipFile from zipfile import ZipFile
from UM.Logger import Logger
from cura.Backups.Backup import Backup
class BackupsManager: class BackupsManager:
""" """
@ -9,15 +12,17 @@ class BackupsManager:
Backups themselves are represented in a different class. Backups themselves are represented in a different class.
""" """
def __init__(self):
pass
def createBackup(self) -> ("ZipFile", dict): def createBackup(self) -> ("ZipFile", dict):
""" """
Get a backup of the current configuration. 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). :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: 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 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. :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.