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.
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)

View File

@ -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()

View File

@ -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.

View File

@ -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.