Refactor the create backup implementation to CreateBackupJob

This commit is contained in:
Nino van Hooff 2020-03-10 13:34:18 +01:00
parent 244d018a2e
commit ed5c2b3f43
3 changed files with 129 additions and 101 deletions

View file

@ -0,0 +1,122 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import json
import threading
from typing import Any, Dict, Optional
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
from datetime import datetime
from UM.Job import Job
from UM.Logger import Logger
from UM.Message import Message
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
from UM.i18n import i18nCatalog
from cura.CuraApplication import CuraApplication
from plugins.Toolbox.src.UltimakerCloudScope import UltimakerCloudScope
catalog = i18nCatalog("cura")
class CreateBackupJob(Job):
"""Creates backup zip, requests upload url and uploads the backup file to cloud storage."""
MESSAGE_TITLE = catalog.i18nc("@info:title", "Backups")
def __init__(self, api_backup_url: str) -> None:
""" Create a new backup Job. start the job by calling start()
:param api_backup_url: The url of the 'backups' endpoint of the Cura Drive Api
"""
super().__init__()
self._api_backup_url = api_backup_url
self._jsonCloudScope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance()))
self._backup_zip = None
self._upload_success = False
self._upload_success_available = threading.Event()
self.backup_upload_error_message = ""
def run(self) -> None:
upload_message = Message(catalog.i18nc("@info:backup_status", "Creating your backup..."), title = self.MESSAGE_TITLE, progress = -1)
upload_message.show()
CuraApplication.getInstance().processEvents()
cura_api = CuraApplication.getInstance().getCuraAPI()
self._backup_zip, backup_meta_data = cura_api.backups.createBackup()
if not self._backup_zip or not backup_meta_data:
self.backup_upload_error_message = "Could not create backup."
upload_message.hide()
return
upload_message.setText(catalog.i18nc("@info:backup_status", "Uploading your backup..."))
CuraApplication.getInstance().processEvents()
# Create an upload entry for the backup.
timestamp = datetime.now().isoformat()
backup_meta_data["description"] = "{}.backup.{}.cura.zip".format(timestamp, backup_meta_data["cura_release"])
self._requestUploadSlot(backup_meta_data, len(self._backup_zip))
self._upload_success_available.wait()
upload_message.hide()
def _requestUploadSlot(self, backup_metadata: Dict[str, Any], backup_size: int) -> None:
"""Request a backup upload slot from the API.
:param backup_metadata: A dict containing some meta data about the backup.
:param backup_size: The size of the backup file in bytes.
:return: The upload URL for the actual backup file if successful, otherwise None.
"""
payload = json.dumps({"data": {"backup_size": backup_size,
"metadata": backup_metadata
}
}).encode()
HttpRequestManager.getInstance().put(
self._api_backup_url,
data = payload,
callback = self._onUploadSlotCompleted,
error_callback = self._onUploadSlotCompleted,
scope = self._jsonCloudScope)
def _onUploadSlotCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None) -> None:
if error is not None:
Logger.warning(str(error))
self.backup_upload_error_message = "Could not upload backup."
self._upload_success_available.set()
return
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) >= 300:
Logger.warning("Could not request backup upload: %s", HttpRequestManager.readText(reply))
self.backup_upload_error_message = "Could not upload backup."
self._upload_success_available.set()
return
backup_upload_url = HttpRequestManager.readJSON(reply)["data"]["upload_url"]
# Upload the backup to storage.
HttpRequestManager.getInstance().put(
backup_upload_url,
data=self._backup_zip,
callback=self._uploadFinishedCallback,
error_callback=self._uploadFinishedCallback
)
def _uploadFinishedCallback(self, reply: QNetworkReply, error: QNetworkReply.NetworkError = None):
self.backup_upload_error_text = HttpRequestManager.readText(reply)
if HttpRequestManager.replyIndicatesSuccess(reply, error):
self._upload_success = True
Message(catalog.i18nc("@info:backup_status", "Your backup has finished uploading."), title = self.MESSAGE_TITLE).show()
else:
self.backup_upload_error_text = self.backup_upload_error_text
Logger.log("w", "Could not upload backup file: %s", self.backup_upload_error_text)
Message(catalog.i18nc("@info:backup_status", "There was an error while uploading your backup."),
title=self.MESSAGE_TITLE).show()
self._upload_success_available.set()

View file

@ -3,8 +3,6 @@
import base64 import base64
import hashlib import hashlib
import json
from datetime import datetime
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from typing import Any, Optional, List, Dict, Callable from typing import Any, Optional, List, Dict, Callable
@ -17,7 +15,7 @@ from plugins.Toolbox.src.UltimakerCloudScope import UltimakerCloudScope
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
from .UploadBackupJob import UploadBackupJob from .CreateBackupJob import CreateBackupJob
from .Settings import Settings from .Settings import Settings
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
@ -40,21 +38,20 @@ class DriveApiService:
def __init__(self) -> None: def __init__(self) -> None:
self._cura_api = CuraApplication.getInstance().getCuraAPI() self._cura_api = CuraApplication.getInstance().getCuraAPI()
self._jsonCloudScope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) self._jsonCloudScope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance()))
self._current_backup_zip_file = None
self.creatingStateChanged.connect(self._creatingStateChanged)
def getBackups(self, changed: Callable[[List], None]): def getBackups(self, changed: Callable[[List], None]):
def callback(reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None): def callback(reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None):
if error is not None: if error is not None:
Logger.log("w", "Could not get backups: " + str(error)) Logger.log("w", "Could not get backups: " + str(error))
changed([]) changed([])
return
backup_list_response = HttpRequestManager.readJSON(reply) backup_list_response = HttpRequestManager.readJSON(reply)
if "data" not in backup_list_response: if "data" not in backup_list_response:
Logger.log("w", "Could not get backups from remote, actual response body was: %s", Logger.log("w", "Could not get backups from remote, actual response body was: %s",
str(backup_list_response)) str(backup_list_response))
changed([]) # empty list of backups changed([]) # empty list of backups
return
changed(backup_list_response["data"]) changed(backup_list_response["data"])
@ -67,20 +64,11 @@ class DriveApiService:
def createBackup(self) -> None: def createBackup(self) -> None:
self.creatingStateChanged.emit(is_creating = True) self.creatingStateChanged.emit(is_creating = True)
upload_backup_job = CreateBackupJob(self.BACKUP_URL)
upload_backup_job.finished.connect(self._onUploadFinished)
upload_backup_job.start()
# Create the backup. def _onUploadFinished(self, job: "CreateBackupJob") -> None:
backup_zip_file, backup_meta_data = self._cura_api.backups.createBackup()
if not backup_zip_file or not backup_meta_data:
self.creatingStateChanged.emit(is_creating = False, error_message ="Could not create backup.")
return
# Create an upload entry for the backup.
timestamp = datetime.now().isoformat()
backup_meta_data["description"] = "{}.backup.{}.cura.zip".format(timestamp, backup_meta_data["cura_release"])
self._requestBackupUpload(backup_meta_data, len(backup_zip_file))
self._current_backup_zip_file = backup_zip_file
def _onUploadFinished(self, job: "UploadBackupJob") -> None:
if job.backup_upload_error_message != "": if job.backup_upload_error_message != "":
# If the job contains an error message we pass it along so the UI can display it. # If the job contains an error message we pass it along so the UI can display it.
self.creatingStateChanged.emit(is_creating = False, error_message = job.backup_upload_error_message) self.creatingStateChanged.emit(is_creating = False, error_message = job.backup_upload_error_message)
@ -167,46 +155,3 @@ class DriveApiService:
def _onDeleteRequestCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None, callable = None): def _onDeleteRequestCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None, callable = None):
callable(HttpRequestManager.replyIndicatesSuccess(reply, error)) callable(HttpRequestManager.replyIndicatesSuccess(reply, error))
def _requestBackupUpload(self, backup_metadata: Dict[str, Any], backup_size: int) -> None:
"""Request a backup upload slot from the API.
:param backup_metadata: A dict containing some meta data about the backup.
:param backup_size: The size of the backup file in bytes.
:return: The upload URL for the actual backup file if successful, otherwise None.
"""
payload = json.dumps({"data": {"backup_size": backup_size,
"metadata": backup_metadata
}
}).encode()
HttpRequestManager.getInstance().put(
self.BACKUP_URL,
data = payload,
callback = self._onBackupUploadSlotCompleted,
error_callback = self._onBackupUploadSlotCompleted,
scope = self._jsonCloudScope)
def _onBackupUploadSlotCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None) -> None:
if error is not None:
Logger.warning(str(error))
self.creatingStateChanged.emit(is_creating=False, error_message="Could not upload backup.")
return
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) >= 300:
Logger.warning("Could not request backup upload: %s", HttpRequestManager.readText(reply))
self.creatingStateChanged.emit(is_creating=False, error_message="Could not upload backup.")
return
backup_upload_url = HttpRequestManager.readJSON(reply)["data"]["upload_url"]
# Upload the backup to storage.
upload_backup_job = UploadBackupJob(backup_upload_url, self._current_backup_zip_file)
upload_backup_job.finished.connect(self._onUploadFinished)
upload_backup_job.start()
def _creatingStateChanged(self, is_creating: bool = False, error_message: str = None) -> None:
"""Cleanup after a backup is not needed anymore"""
if not is_creating:
self._current_backup_zip_file = None

View file

@ -1,39 +0,0 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import requests
from UM.Job import Job
from UM.Logger import Logger
from UM.Message import Message
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
class UploadBackupJob(Job):
MESSAGE_TITLE = catalog.i18nc("@info:title", "Backups")
# This job is responsible for uploading the backup file to cloud storage.
# As it can take longer than some other tasks, we schedule this using a Cura Job.
def __init__(self, signed_upload_url: str, backup_zip: bytes) -> None:
super().__init__()
self._signed_upload_url = signed_upload_url
self._backup_zip = backup_zip
self._upload_success = False
self.backup_upload_error_message = ""
def run(self) -> None:
upload_message = Message(catalog.i18nc("@info:backup_status", "Uploading your backup..."), title = self.MESSAGE_TITLE, progress = -1)
upload_message.show()
backup_upload = requests.put(self._signed_upload_url, data = self._backup_zip)
upload_message.hide()
if backup_upload.status_code >= 300:
self.backup_upload_error_message = backup_upload.text
Logger.log("w", "Could not upload backup file: %s", backup_upload.text)
Message(catalog.i18nc("@info:backup_status", "There was an error while uploading your backup."), title = self.MESSAGE_TITLE).show()
else:
self._upload_success = True
Message(catalog.i18nc("@info:backup_status", "Your backup has finished uploading."), title = self.MESSAGE_TITLE).show()