From 27902fe38fb6e3a32114f1ec705b4f92323c5c91 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Tue, 25 Feb 2020 14:41:02 +0100 Subject: [PATCH 01/22] Use JsonDecorator scope in the Toolbox and CloudSync Improves security by disallowing other content than json for requests CURA-7150 --- plugins/Toolbox/src/CloudSync/CloudApiClient.py | 3 ++- plugins/Toolbox/src/CloudSync/CloudPackageChecker.py | 6 ++++-- plugins/Toolbox/src/CloudSync/DownloadPresenter.py | 3 ++- plugins/Toolbox/src/Toolbox.py | 3 ++- plugins/Toolbox/src/UltimakerCloudScope.py | 9 ++++++--- 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/plugins/Toolbox/src/CloudSync/CloudApiClient.py b/plugins/Toolbox/src/CloudSync/CloudApiClient.py index 6c14aea26c..ba1cbab624 100644 --- a/plugins/Toolbox/src/CloudSync/CloudApiClient.py +++ b/plugins/Toolbox/src/CloudSync/CloudApiClient.py @@ -1,5 +1,6 @@ from UM.Logger import Logger from UM.TaskManagement.HttpRequestManager import HttpRequestManager +from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope from cura.CuraApplication import CuraApplication from ..CloudApiModel import CloudApiModel from ..UltimakerCloudScope import UltimakerCloudScope @@ -26,7 +27,7 @@ class CloudApiClient: if self.__instance is not None: raise RuntimeError("This is a Singleton. use getInstance()") - self._scope = UltimakerCloudScope(app) # type: UltimakerCloudScope + self._scope = JsonDecoratorScope(UltimakerCloudScope(app)) # type: JsonDecoratorScope app.getPackageManager().packageInstalled.connect(self._onPackageInstalled) diff --git a/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py b/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py index 7d223c87c6..7404489aee 100644 --- a/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py +++ b/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py @@ -11,6 +11,7 @@ from UM import i18nCatalog from UM.Logger import Logger from UM.Message import Message from UM.Signal import Signal +from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope from cura.CuraApplication import CuraApplication, ApplicationMetadata from ..CloudApiModel import CloudApiModel from .SubscribedPackagesModel import SubscribedPackagesModel @@ -18,13 +19,14 @@ from ..UltimakerCloudScope import UltimakerCloudScope from typing import List, Dict, Any + class CloudPackageChecker(QObject): def __init__(self, application: CuraApplication) -> None: super().__init__() self.discrepancies = Signal() # Emits SubscribedPackagesModel self._application = application # type: CuraApplication - self._scope = UltimakerCloudScope(application) + self._scope = JsonDecoratorScope(UltimakerCloudScope(application)) self._model = SubscribedPackagesModel() self._application.initializationFinished.connect(self._onAppInitialized) @@ -106,4 +108,4 @@ class CloudPackageChecker(QObject): def _onSyncButtonClicked(self, sync_message: Message, sync_message_action: str) -> None: sync_message.hide() - self.discrepancies.emit(self._model) \ No newline at end of file + self.discrepancies.emit(self._model) diff --git a/plugins/Toolbox/src/CloudSync/DownloadPresenter.py b/plugins/Toolbox/src/CloudSync/DownloadPresenter.py index 743d96c574..06004df5da 100644 --- a/plugins/Toolbox/src/CloudSync/DownloadPresenter.py +++ b/plugins/Toolbox/src/CloudSync/DownloadPresenter.py @@ -11,6 +11,7 @@ from UM.Logger import Logger from UM.Message import Message from UM.Signal import Signal from UM.TaskManagement.HttpRequestManager import HttpRequestManager +from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope from cura.CuraApplication import CuraApplication from .SubscribedPackagesModel import SubscribedPackagesModel from ..UltimakerCloudScope import UltimakerCloudScope @@ -29,7 +30,7 @@ class DownloadPresenter: self.done = Signal() self._app = app - self._scope = UltimakerCloudScope(app) + self._scope = JsonDecoratorScope(UltimakerCloudScope(app)) self._started = False self._progress_message = self._createProgressMessage() diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index 55c6ba223b..9057538835 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -12,6 +12,7 @@ from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkRepl from UM.Logger import Logger from UM.PluginRegistry import PluginRegistry from UM.Extension import Extension +from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope from UM.i18n import i18nCatalog from UM.Version import Version @@ -54,7 +55,7 @@ class Toolbox(QObject, Extension): self._download_request_data = None # type: Optional[HttpRequestData] self._download_progress = 0 # type: float self._is_downloading = False # type: bool - self._scope = UltimakerCloudScope(application) # type: UltimakerCloudScope + self._scope = JsonDecoratorScope(UltimakerCloudScope(application)) # type: JsonDecoratorScope self._request_urls = {} # type: Dict[str, str] self._to_update = [] # type: List[str] # Package_ids that are waiting to be updated diff --git a/plugins/Toolbox/src/UltimakerCloudScope.py b/plugins/Toolbox/src/UltimakerCloudScope.py index 14583d7d59..257e97b1af 100644 --- a/plugins/Toolbox/src/UltimakerCloudScope.py +++ b/plugins/Toolbox/src/UltimakerCloudScope.py @@ -6,10 +6,13 @@ from cura.API import Account from cura.CuraApplication import CuraApplication -## Add a Authorization header to the request for Ultimaker Cloud Api requests. -# When the user is not logged in or a token is not available, a warning will be logged -# Also add the user agent headers (see DefaultUserAgentScope) class UltimakerCloudScope(DefaultUserAgentScope): + """Add an Authorization header to the request for Ultimaker Cloud Api requests. + + When the user is not logged in or a token is not available, a warning will be logged + Also add the user agent headers (see DefaultUserAgentScope) + """ + def __init__(self, application: CuraApplication): super().__init__(application) api = application.getCuraAPI() From 77590ad0e25b900756322a536420faf7f95741cf Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Tue, 25 Feb 2020 14:42:12 +0100 Subject: [PATCH 02/22] Disable SSL checking in debug mode Allows inspecting web traffic during development CURA-7150 --- cura_app.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cura_app.py b/cura_app.py index 38d1149080..1f0c4a0743 100755 --- a/cura_app.py +++ b/cura_app.py @@ -15,6 +15,8 @@ import sys import Arcus # @UnusedImport import Savitar # @UnusedImport +from PyQt5.QtNetwork import QSslConfiguration, QSslSocket + from UM.Platform import Platform from cura import ApplicationMetadata from cura.ApplicationMetadata import CuraAppName @@ -209,5 +211,10 @@ if Platform.isLinux() and getattr(sys, "frozen", False): import trimesh.exchange.load os.environ["LD_LIBRARY_PATH"] = old_env +if ApplicationMetadata.CuraDebugMode: + ssl_conf = QSslConfiguration.defaultConfiguration() + ssl_conf.setPeerVerifyMode(QSslSocket.VerifyNone) + QSslConfiguration.setDefaultConfiguration(ssl_conf) + app = CuraApplication() app.run() From 86fb0383decb1469c44004d5e1a3c5c1ea2d55ef Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Fri, 28 Feb 2020 14:01:11 +0100 Subject: [PATCH 03/22] Refactor refresh backups call to use HttpRequestManager --- plugins/CuraDrive/src/DriveApiService.py | 44 +++++++++---------- plugins/CuraDrive/src/DrivePluginExtension.py | 5 ++- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/plugins/CuraDrive/src/DriveApiService.py b/plugins/CuraDrive/src/DriveApiService.py index d8349ccc29..35f8d95ca6 100644 --- a/plugins/CuraDrive/src/DriveApiService.py +++ b/plugins/CuraDrive/src/DriveApiService.py @@ -5,14 +5,19 @@ import base64 import hashlib from datetime import datetime from tempfile import NamedTemporaryFile -from typing import Any, Optional, List, Dict +from typing import Any, Optional, List, Dict, Callable import requests from UM.Logger import Logger from UM.Message import Message from UM.Signal import Signal, signalemitter +from UM.TaskManagement.HttpRequestManager import HttpRequestManager +from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope from cura.CuraApplication import CuraApplication +from plugins.Toolbox.src.UltimakerCloudScope import UltimakerCloudScope + +from PyQt5.QtNetwork import QNetworkReply from .UploadBackupJob import UploadBackupJob from .Settings import Settings @@ -34,33 +39,24 @@ class DriveApiService: def __init__(self) -> None: self._cura_api = CuraApplication.getInstance().getCuraAPI() + self._scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) - def getBackups(self) -> List[Dict[str, Any]]: - access_token = self._cura_api.account.accessToken - if not access_token: - Logger.log("w", "Could not get access token.") - return [] - try: - backup_list_request = requests.get(self.BACKUP_URL, headers = { - "Authorization": "Bearer {}".format(access_token) - }) - except requests.exceptions.ConnectionError: - Logger.logException("w", "Unable to connect with the server.") - return [] + def getBackups(self, changed: Callable): + def callback(reply: QNetworkReply): + backup_list_response = HttpRequestManager.readJSON(reply) + if "data" not in backup_list_response: + Logger.log("w", "Could not get backups from remote, actual response body was: %s", + str(backup_list_response)) + changed([]) # empty list of backups - # HTTP status 300s mean redirection. 400s and 500s are errors. - # Technically 300s are not errors, but the use case here relies on "requests" to handle redirects automatically. - if backup_list_request.status_code >= 300: - Logger.log("w", "Could not get backups list from remote: %s", backup_list_request.text) - Message(catalog.i18nc("@info:backup_status", "There was an error listing your backups."), title = catalog.i18nc("@info:title", "Backup")).show() - return [] + changed(backup_list_response["data"]) - backup_list_response = backup_list_request.json() - if "data" not in backup_list_response: - Logger.log("w", "Could not get backups from remote, actual response body was: %s", str(backup_list_response)) - return [] + HttpRequestManager.getInstance().get( + self.BACKUP_URL, + callback=callback, + scope=self._scope + ) - return backup_list_response["data"] def createBackup(self) -> None: self.creatingStateChanged.emit(is_creating = True) diff --git a/plugins/CuraDrive/src/DrivePluginExtension.py b/plugins/CuraDrive/src/DrivePluginExtension.py index bcc326a133..31bf0bc933 100644 --- a/plugins/CuraDrive/src/DrivePluginExtension.py +++ b/plugins/CuraDrive/src/DrivePluginExtension.py @@ -133,7 +133,10 @@ class DrivePluginExtension(QObject, Extension): @pyqtSlot(name = "refreshBackups") def refreshBackups(self) -> None: - self._backups = self._drive_api_service.getBackups() + self._drive_api_service.getBackups(self._backupsChangedCallback) + + def _backupsChangedCallback(self, backups): + self.backups = backups self.backupsChanged.emit() @pyqtProperty(bool, notify = restoringStateChanged) From 234acf0904f1a1a85cbc423c8c64b4b057fcadf3 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Fri, 28 Feb 2020 16:25:28 +0100 Subject: [PATCH 04/22] Fix bug: typo in variable name --- plugins/CuraDrive/src/DrivePluginExtension.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/CuraDrive/src/DrivePluginExtension.py b/plugins/CuraDrive/src/DrivePluginExtension.py index 31bf0bc933..210d6ae8ea 100644 --- a/plugins/CuraDrive/src/DrivePluginExtension.py +++ b/plugins/CuraDrive/src/DrivePluginExtension.py @@ -136,7 +136,7 @@ class DrivePluginExtension(QObject, Extension): self._drive_api_service.getBackups(self._backupsChangedCallback) def _backupsChangedCallback(self, backups): - self.backups = backups + self._backups = backups self.backupsChanged.emit() @pyqtProperty(bool, notify = restoringStateChanged) From 4f2827e1bf903a73d9f25602a64a4c03ae87b7d5 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Fri, 28 Feb 2020 16:25:43 +0100 Subject: [PATCH 05/22] Refactor restore backups call to use HttpRequestManager --- plugins/CuraDrive/src/DriveApiService.py | 42 ++++++++++++++++-------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/plugins/CuraDrive/src/DriveApiService.py b/plugins/CuraDrive/src/DriveApiService.py index 35f8d95ca6..0a2d406d59 100644 --- a/plugins/CuraDrive/src/DriveApiService.py +++ b/plugins/CuraDrive/src/DriveApiService.py @@ -17,7 +17,7 @@ from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope from cura.CuraApplication import CuraApplication from plugins.Toolbox.src.UltimakerCloudScope import UltimakerCloudScope -from PyQt5.QtNetwork import QNetworkReply +from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest from .UploadBackupJob import UploadBackupJob from .Settings import Settings @@ -30,6 +30,7 @@ catalog = i18nCatalog("cura") @signalemitter class DriveApiService: BACKUP_URL = "{}/backups".format(Settings.DRIVE_API_URL) + DISK_WRITE_BUFFER_SIZE = 512 * 1024 # Emit signal when restoring backup started or finished. restoringStateChanged = Signal() @@ -40,9 +41,14 @@ class DriveApiService: def __init__(self) -> None: self._cura_api = CuraApplication.getInstance().getCuraAPI() self._scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) + self._in_progress_backup = None def getBackups(self, changed: Callable): - def callback(reply: QNetworkReply): + def callback(reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None): + if error is not None: + Logger.log("w", "Could not get backups: " + str(error)) + changed([]) + backup_list_response = HttpRequestManager.readJSON(reply) if "data" not in backup_list_response: Logger.log("w", "Could not get backups from remote, actual response body was: %s", @@ -94,22 +100,32 @@ class DriveApiService: # If there is no download URL, we can't restore the backup. return self._emitRestoreError() - try: - download_package = requests.get(download_url, stream = True) - except requests.exceptions.ConnectionError: - Logger.logException("e", "Unable to connect with the server") - return self._emitRestoreError() + def finishedCallback(reply: QNetworkReply, bu=backup) -> None: + self._onRestoreRequestCompleted(reply, None, bu) - if download_package.status_code >= 300: - # Something went wrong when attempting to download the backup. - Logger.log("w", "Could not download backup from url %s: %s", download_url, download_package.text) - return self._emitRestoreError() + HttpRequestManager.getInstance().get( + url = download_url, + callback = finishedCallback, + error_callback = self._onRestoreRequestCompleted + ) + + def _onRestoreRequestCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None, backup = None): + if error is not None or reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: + Logger.log("w", + "Requesting backup failed, response code %s while trying to connect to %s", + reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url()) + self._emitRestoreError() + return # We store the file in a temporary path fist to ensure integrity. temporary_backup_file = NamedTemporaryFile(delete = False) with open(temporary_backup_file.name, "wb") as write_backup: - for chunk in download_package: - write_backup.write(chunk) + app = CuraApplication.getInstance() + bytes_read = reply.read(DriveApiService.DISK_WRITE_BUFFER_SIZE) + while bytes_read: + write_backup.write(bytes_read) + bytes_read = reply.read(DriveApiService.DISK_WRITE_BUFFER_SIZE) + app.processEvents() if not self._verifyMd5Hash(temporary_backup_file.name, backup.get("md5_hash", "")): # Don't restore the backup if the MD5 hashes do not match. From 19c3f765f585908d1921d74fbc71c21c1e3d65cf Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Fri, 28 Feb 2020 17:08:31 +0100 Subject: [PATCH 06/22] Refactor delete backups call to use HttpRequestManager --- plugins/CuraDrive/src/DriveApiService.py | 48 ++++++++++--------- plugins/CuraDrive/src/DrivePluginExtension.py | 7 ++- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/plugins/CuraDrive/src/DriveApiService.py b/plugins/CuraDrive/src/DriveApiService.py index 0a2d406d59..0168d8de2b 100644 --- a/plugins/CuraDrive/src/DriveApiService.py +++ b/plugins/CuraDrive/src/DriveApiService.py @@ -10,7 +10,6 @@ from typing import Any, Optional, List, Dict, Callable import requests from UM.Logger import Logger -from UM.Message import Message from UM.Signal import Signal, signalemitter from UM.TaskManagement.HttpRequestManager import HttpRequestManager from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope @@ -40,10 +39,10 @@ class DriveApiService: def __init__(self) -> None: self._cura_api = CuraApplication.getInstance().getCuraAPI() - self._scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) + self._jsonCloudScope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) self._in_progress_backup = None - def getBackups(self, changed: Callable): + def getBackups(self, changed: Callable[[List], None]): def callback(reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None): if error is not None: Logger.log("w", "Could not get backups: " + str(error)) @@ -59,8 +58,9 @@ class DriveApiService: HttpRequestManager.getInstance().get( self.BACKUP_URL, - callback=callback, - scope=self._scope + callback= callback, + error_callback = callback, + scope=self._jsonCloudScope ) @@ -110,7 +110,7 @@ class DriveApiService: ) def _onRestoreRequestCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None, backup = None): - if error is not None or reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: + if not DriveApiService._replyIndicatesSuccess(reply, error): Logger.log("w", "Requesting backup failed, response code %s while trying to connect to %s", reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url()) @@ -153,24 +153,28 @@ class DriveApiService: local_md5_hash = base64.b64encode(hashlib.md5(read_backup.read()).digest(), altchars = b"_-").decode("utf-8") return known_hash == local_md5_hash - def deleteBackup(self, backup_id: str) -> bool: - access_token = self._cura_api.account.accessToken - if not access_token: - Logger.log("w", "Could not get access token.") - return False + @staticmethod + def _replyIndicatesSuccess(reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None): + """Returns whether reply status code indicates success and error is None""" + return error is None and 200 <= reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) < 300 - try: - delete_backup = requests.delete("{}/{}".format(self.BACKUP_URL, backup_id), headers = { - "Authorization": "Bearer {}".format(access_token) - }) - except requests.exceptions.ConnectionError: - Logger.logException("e", "Unable to connect with the server") - return False + def deleteBackup(self, backup_id: str, callable: Callable[[bool], None]): - if delete_backup.status_code >= 300: - Logger.log("w", "Could not delete backup: %s", delete_backup.text) - return False - return True + def finishedCallback(reply: QNetworkReply, ca=callable) -> None: + self._onDeleteRequestCompleted(reply, None, ca) + + def errorCallback(reply: QNetworkReply, error: QNetworkReply.NetworkError, ca=callable) -> None: + self._onDeleteRequestCompleted(reply, error, ca) + + HttpRequestManager.getInstance().delete( + url = "{}/{}".format(self.BACKUP_URL, backup_id), + callback = finishedCallback, + error_callback = errorCallback, + scope= self._jsonCloudScope + ) + + def _onDeleteRequestCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None, callable = None): + callable(DriveApiService._replyIndicatesSuccess(reply, error)) # Request a backup upload slot from the API. # \param backup_metadata: A dict containing some meta data about the backup. diff --git a/plugins/CuraDrive/src/DrivePluginExtension.py b/plugins/CuraDrive/src/DrivePluginExtension.py index 210d6ae8ea..34f30d8b3d 100644 --- a/plugins/CuraDrive/src/DrivePluginExtension.py +++ b/plugins/CuraDrive/src/DrivePluginExtension.py @@ -161,5 +161,8 @@ class DrivePluginExtension(QObject, Extension): @pyqtSlot(str, name = "deleteBackup") def deleteBackup(self, backup_id: str) -> None: - self._drive_api_service.deleteBackup(backup_id) - self.refreshBackups() + self._drive_api_service.deleteBackup(backup_id, self._backupDeletedCallback) + + def _backupDeletedCallback(self, success: bool): + if success: + self.refreshBackups() From 762f699f6462b861496462152035246aa14e3c19 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Fri, 28 Feb 2020 17:37:21 +0100 Subject: [PATCH 07/22] Refactor UploadBackupJob to use HttpRequestManager --- plugins/CuraDrive/src/DriveApiService.py | 14 +++------- plugins/CuraDrive/src/UploadBackupJob.py | 33 +++++++++++++++--------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/plugins/CuraDrive/src/DriveApiService.py b/plugins/CuraDrive/src/DriveApiService.py index 0168d8de2b..167eae4424 100644 --- a/plugins/CuraDrive/src/DriveApiService.py +++ b/plugins/CuraDrive/src/DriveApiService.py @@ -63,7 +63,6 @@ class DriveApiService: scope=self._jsonCloudScope ) - def createBackup(self) -> None: self.creatingStateChanged.emit(is_creating = True) @@ -87,9 +86,9 @@ class DriveApiService: upload_backup_job.start() def _onUploadFinished(self, job: "UploadBackupJob") -> None: - if job.backup_upload_error_message != "": + if job.backup_upload_error_text != "": # 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_text) else: self.creatingStateChanged.emit(is_creating = False) @@ -110,7 +109,7 @@ class DriveApiService: ) def _onRestoreRequestCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None, backup = None): - if not DriveApiService._replyIndicatesSuccess(reply, error): + if not HttpRequestManager.replyIndicatesSuccess(reply, error): Logger.log("w", "Requesting backup failed, response code %s while trying to connect to %s", reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url()) @@ -153,11 +152,6 @@ class DriveApiService: local_md5_hash = base64.b64encode(hashlib.md5(read_backup.read()).digest(), altchars = b"_-").decode("utf-8") return known_hash == local_md5_hash - @staticmethod - def _replyIndicatesSuccess(reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None): - """Returns whether reply status code indicates success and error is None""" - return error is None and 200 <= reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) < 300 - def deleteBackup(self, backup_id: str, callable: Callable[[bool], None]): def finishedCallback(reply: QNetworkReply, ca=callable) -> None: @@ -174,7 +168,7 @@ class DriveApiService: ) def _onDeleteRequestCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None, callable = None): - callable(DriveApiService._replyIndicatesSuccess(reply, error)) + callable(HttpRequestManager.replyIndicatesSuccess(reply, error)) # Request a backup upload slot from the API. # \param backup_metadata: A dict containing some meta data about the backup. diff --git a/plugins/CuraDrive/src/UploadBackupJob.py b/plugins/CuraDrive/src/UploadBackupJob.py index 2e76ed9b4b..5ac48c7512 100644 --- a/plugins/CuraDrive/src/UploadBackupJob.py +++ b/plugins/CuraDrive/src/UploadBackupJob.py @@ -1,11 +1,11 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. - -import requests +from PyQt5.QtNetwork import QNetworkReply from UM.Job import Job from UM.Logger import Logger from UM.Message import Message +from UM.TaskManagement.HttpRequestManager import HttpRequestManager from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") @@ -21,21 +21,30 @@ class UploadBackupJob(Job): self._signed_upload_url = signed_upload_url self._backup_zip = backup_zip self._upload_success = False - self.backup_upload_error_message = "" + self.backup_upload_error_text = "" + self._message = None def run(self) -> None: - upload_message = Message(catalog.i18nc("@info:backup_status", "Uploading your backup..."), title = self.MESSAGE_TITLE, progress = -1) - upload_message.show() + self._message = Message(catalog.i18nc("@info:backup_status", "Uploading your backup..."), title = self.MESSAGE_TITLE, progress = -1) + self._message.show() - backup_upload = requests.put(self._signed_upload_url, data = self._backup_zip) - upload_message.hide() + HttpRequestManager.getInstance().put( + self._signed_upload_url, + data = self._backup_zip + ) - 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: + def uploadFinishedCallback(self, reply: QNetworkReply, error: QNetworkReply.NetworkError): + self._message.hide() + + 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.finished.emit(self) From 932b12e66c6be25f3467c505738b8c9a694a7043 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Mon, 2 Mar 2020 11:58:12 +0100 Subject: [PATCH 08/22] Convert doxygen to rst for DriveApiService --- plugins/CuraDrive/src/DriveApiService.py | 29 +++++++++++++++--------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/plugins/CuraDrive/src/DriveApiService.py b/plugins/CuraDrive/src/DriveApiService.py index 167eae4424..3a94cf7aa0 100644 --- a/plugins/CuraDrive/src/DriveApiService.py +++ b/plugins/CuraDrive/src/DriveApiService.py @@ -25,17 +25,18 @@ from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") -## The DriveApiService is responsible for interacting with the CuraDrive API and Cura's backup handling. @signalemitter class DriveApiService: + """The DriveApiService is responsible for interacting with the CuraDrive API and Cura's backup handling.""" + BACKUP_URL = "{}/backups".format(Settings.DRIVE_API_URL) DISK_WRITE_BUFFER_SIZE = 512 * 1024 - # Emit signal when restoring backup started or finished. restoringStateChanged = Signal() + """Emits signal when restoring backup started or finished.""" - # Emit signal when creating backup started or finished. creatingStateChanged = Signal() + """Emits signal when creating backup started or finished.""" def __init__(self) -> None: self._cura_api = CuraApplication.getInstance().getCuraAPI() @@ -142,12 +143,15 @@ class DriveApiService: error_message = catalog.i18nc("@info:backup_status", "There was an error trying to restore your backup.")) - # Verify the MD5 hash of a file. - # \param file_path Full path to the file. - # \param known_hash The known MD5 hash of the file. - # \return: Success or not. @staticmethod def _verifyMd5Hash(file_path: str, known_hash: str) -> bool: + """Verify the MD5 hash of a file. + + :param file_path: Full path to the file. + :param known_hash: The known MD5 hash of the file. + :return: Success or not. + """ + with open(file_path, "rb") as read_backup: local_md5_hash = base64.b64encode(hashlib.md5(read_backup.read()).digest(), altchars = b"_-").decode("utf-8") return known_hash == local_md5_hash @@ -170,11 +174,14 @@ class DriveApiService: def _onDeleteRequestCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None, callable = None): callable(HttpRequestManager.replyIndicatesSuccess(reply, error)) - # 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. def _requestBackupUpload(self, backup_metadata: Dict[str, Any], backup_size: int) -> Optional[str]: + """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. + """ + access_token = self._cura_api.account.accessToken if not access_token: Logger.log("w", "Could not get access token.") From 6c9b9909baa1cd8b379c6ea24c207a8ae8b7c4ac Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Mon, 2 Mar 2020 14:46:33 +0100 Subject: [PATCH 09/22] Fix UploadBackupJob --- plugins/CuraDrive/src/UploadBackupJob.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/CuraDrive/src/UploadBackupJob.py b/plugins/CuraDrive/src/UploadBackupJob.py index 5ac48c7512..598da495bd 100644 --- a/plugins/CuraDrive/src/UploadBackupJob.py +++ b/plugins/CuraDrive/src/UploadBackupJob.py @@ -29,11 +29,13 @@ class UploadBackupJob(Job): self._message.show() HttpRequestManager.getInstance().put( - self._signed_upload_url, - data = self._backup_zip + self._signed_upload_url, + data = self._backup_zip, + callback = self.uploadFinishedCallback, + error_callback = self.uploadFinishedCallback ) - def uploadFinishedCallback(self, reply: QNetworkReply, error: QNetworkReply.NetworkError): + def uploadFinishedCallback(self, reply: QNetworkReply, error: QNetworkReply.NetworkError = None): self._message.hide() self.backup_upload_error_text = HttpRequestManager.readText(reply) From 7243dc63a4f8815f80d244462e09734e485913f4 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Mon, 2 Mar 2020 15:12:42 +0100 Subject: [PATCH 10/22] Revert: Refactor UploadBackupJob to use HttpRequestManager It doesn't make sense to have a Job using HttpRequestManager because both are async and Note that the job itself should not emit finished, the JobQueue does that. --- plugins/CuraDrive/src/DriveApiService.py | 4 +-- plugins/CuraDrive/src/UploadBackupJob.py | 37 ++++++++---------------- 2 files changed, 14 insertions(+), 27 deletions(-) diff --git a/plugins/CuraDrive/src/DriveApiService.py b/plugins/CuraDrive/src/DriveApiService.py index 3a94cf7aa0..ba43d01187 100644 --- a/plugins/CuraDrive/src/DriveApiService.py +++ b/plugins/CuraDrive/src/DriveApiService.py @@ -87,9 +87,9 @@ class DriveApiService: upload_backup_job.start() def _onUploadFinished(self, job: "UploadBackupJob") -> None: - if job.backup_upload_error_text != "": + if job.backup_upload_error_message != "": # 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_text) + self.creatingStateChanged.emit(is_creating = False, error_message = job.backup_upload_error_message) else: self.creatingStateChanged.emit(is_creating = False) diff --git a/plugins/CuraDrive/src/UploadBackupJob.py b/plugins/CuraDrive/src/UploadBackupJob.py index 598da495bd..8ade697cb3 100644 --- a/plugins/CuraDrive/src/UploadBackupJob.py +++ b/plugins/CuraDrive/src/UploadBackupJob.py @@ -1,11 +1,11 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from PyQt5.QtNetwork import QNetworkReply + +import requests from UM.Job import Job from UM.Logger import Logger from UM.Message import Message -from UM.TaskManagement.HttpRequestManager import HttpRequestManager from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") @@ -21,32 +21,19 @@ class UploadBackupJob(Job): self._signed_upload_url = signed_upload_url self._backup_zip = backup_zip self._upload_success = False - self.backup_upload_error_text = "" - self._message = None + self.backup_upload_error_message = "" def run(self) -> None: - self._message = Message(catalog.i18nc("@info:backup_status", "Uploading your backup..."), title = self.MESSAGE_TITLE, progress = -1) - self._message.show() + upload_message = Message(catalog.i18nc("@info:backup_status", "Uploading your backup..."), title = self.MESSAGE_TITLE, progress = -1) + upload_message.show() - HttpRequestManager.getInstance().put( - self._signed_upload_url, - data = self._backup_zip, - callback = self.uploadFinishedCallback, - error_callback = self.uploadFinishedCallback - ) + backup_upload = requests.put(self._signed_upload_url, data = self._backup_zip) + upload_message.hide() - def uploadFinishedCallback(self, reply: QNetworkReply, error: QNetworkReply.NetworkError = None): - self._message.hide() - - self.backup_upload_error_text = HttpRequestManager.readText(reply) - - if HttpRequestManager.replyIndicatesSuccess(reply, error): + 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() - 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.finished.emit(self) From 244d018a2eedb41d92339c6c3d46a60424fc30fe Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Mon, 2 Mar 2020 15:14:04 +0100 Subject: [PATCH 11/22] Refactor createBackup to use HttpRequestManager --- plugins/CuraDrive/src/DriveApiService.py | 82 +++++++++++++----------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/plugins/CuraDrive/src/DriveApiService.py b/plugins/CuraDrive/src/DriveApiService.py index ba43d01187..922ca8afa7 100644 --- a/plugins/CuraDrive/src/DriveApiService.py +++ b/plugins/CuraDrive/src/DriveApiService.py @@ -3,12 +3,11 @@ import base64 import hashlib +import json from datetime import datetime from tempfile import NamedTemporaryFile from typing import Any, Optional, List, Dict, Callable -import requests - from UM.Logger import Logger from UM.Signal import Signal, signalemitter from UM.TaskManagement.HttpRequestManager import HttpRequestManager @@ -41,7 +40,9 @@ class DriveApiService: def __init__(self) -> None: self._cura_api = CuraApplication.getInstance().getCuraAPI() self._jsonCloudScope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) - self._in_progress_backup = None + self._current_backup_zip_file = None + + self.creatingStateChanged.connect(self._creatingStateChanged) def getBackups(self, changed: Callable[[List], None]): def callback(reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None): @@ -76,15 +77,8 @@ class DriveApiService: # 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"]) - backup_upload_url = self._requestBackupUpload(backup_meta_data, len(backup_zip_file)) - if not backup_upload_url: - self.creatingStateChanged.emit(is_creating = False, error_message ="Could not upload backup.") - return - - # Upload the backup to storage. - upload_backup_job = UploadBackupJob(backup_upload_url, backup_zip_file) - upload_backup_job.finished.connect(self._onUploadFinished) - upload_backup_job.start() + 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 != "": @@ -156,12 +150,12 @@ class DriveApiService: local_md5_hash = base64.b64encode(hashlib.md5(read_backup.read()).digest(), altchars = b"_-").decode("utf-8") return known_hash == local_md5_hash - def deleteBackup(self, backup_id: str, callable: Callable[[bool], None]): + def deleteBackup(self, backup_id: str, finishedCallable: Callable[[bool], None]): - def finishedCallback(reply: QNetworkReply, ca=callable) -> None: + def finishedCallback(reply: QNetworkReply, ca=finishedCallable) -> None: self._onDeleteRequestCompleted(reply, None, ca) - def errorCallback(reply: QNetworkReply, error: QNetworkReply.NetworkError, ca=callable) -> None: + def errorCallback(reply: QNetworkReply, error: QNetworkReply.NetworkError, ca=finishedCallable) -> None: self._onDeleteRequestCompleted(reply, error, ca) HttpRequestManager.getInstance().delete( @@ -174,7 +168,7 @@ class DriveApiService: def _onDeleteRequestCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None, callable = None): callable(HttpRequestManager.replyIndicatesSuccess(reply, error)) - def _requestBackupUpload(self, backup_metadata: Dict[str, Any], backup_size: int) -> Optional[str]: + 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. @@ -182,27 +176,37 @@ class DriveApiService: :return: The upload URL for the actual backup file if successful, otherwise None. """ - access_token = self._cura_api.account.accessToken - if not access_token: - Logger.log("w", "Could not get access token.") - return None - try: - backup_upload_request = requests.put( - self.BACKUP_URL, - json = {"data": {"backup_size": backup_size, - "metadata": backup_metadata - } - }, - headers = { - "Authorization": "Bearer {}".format(access_token) - }) - except requests.exceptions.ConnectionError: - Logger.logException("e", "Unable to connect with the server") - return None + payload = json.dumps({"data": {"backup_size": backup_size, + "metadata": backup_metadata + } + }).encode() - # Any status code of 300 or above indicates an error. - if backup_upload_request.status_code >= 300: - Logger.log("w", "Could not request backup upload: %s", backup_upload_request.text) - return None - - return backup_upload_request.json()["data"]["upload_url"] + 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 From ed5c2b3f43c281ca10b9f6f57ae8120b9c57aada Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Tue, 10 Mar 2020 13:34:18 +0100 Subject: [PATCH 12/22] Refactor the create backup implementation to CreateBackupJob --- plugins/CuraDrive/src/CreateBackupJob.py | 122 +++++++++++++++++++++++ plugins/CuraDrive/src/DriveApiService.py | 69 ++----------- plugins/CuraDrive/src/UploadBackupJob.py | 39 -------- 3 files changed, 129 insertions(+), 101 deletions(-) create mode 100644 plugins/CuraDrive/src/CreateBackupJob.py delete mode 100644 plugins/CuraDrive/src/UploadBackupJob.py diff --git a/plugins/CuraDrive/src/CreateBackupJob.py b/plugins/CuraDrive/src/CreateBackupJob.py new file mode 100644 index 0000000000..603733137b --- /dev/null +++ b/plugins/CuraDrive/src/CreateBackupJob.py @@ -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() diff --git a/plugins/CuraDrive/src/DriveApiService.py b/plugins/CuraDrive/src/DriveApiService.py index 922ca8afa7..04f935268b 100644 --- a/plugins/CuraDrive/src/DriveApiService.py +++ b/plugins/CuraDrive/src/DriveApiService.py @@ -3,8 +3,6 @@ import base64 import hashlib -import json -from datetime import datetime from tempfile import NamedTemporaryFile 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 .UploadBackupJob import UploadBackupJob +from .CreateBackupJob import CreateBackupJob from .Settings import Settings from UM.i18n import i18nCatalog @@ -40,21 +38,20 @@ class DriveApiService: def __init__(self) -> None: self._cura_api = CuraApplication.getInstance().getCuraAPI() 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 callback(reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None): if error is not None: Logger.log("w", "Could not get backups: " + str(error)) changed([]) + return backup_list_response = HttpRequestManager.readJSON(reply) if "data" not in backup_list_response: Logger.log("w", "Could not get backups from remote, actual response body was: %s", str(backup_list_response)) changed([]) # empty list of backups + return changed(backup_list_response["data"]) @@ -67,20 +64,11 @@ class DriveApiService: def createBackup(self) -> None: 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. - 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: + def _onUploadFinished(self, job: "CreateBackupJob") -> None: if job.backup_upload_error_message != "": # 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) @@ -167,46 +155,3 @@ class DriveApiService: def _onDeleteRequestCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None, callable = None): 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 diff --git a/plugins/CuraDrive/src/UploadBackupJob.py b/plugins/CuraDrive/src/UploadBackupJob.py deleted file mode 100644 index 8ade697cb3..0000000000 --- a/plugins/CuraDrive/src/UploadBackupJob.py +++ /dev/null @@ -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() From ebfad16508afd8452fb56e02df0c60bd909dbbb9 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Tue, 10 Mar 2020 14:21:52 +0100 Subject: [PATCH 13/22] Refactor the restore backup implementation to RestoreBackupJob --- plugins/CuraDrive/src/CreateBackupJob.py | 12 ++- plugins/CuraDrive/src/DriveApiService.py | 68 +++-------------- plugins/CuraDrive/src/RestoreBackupJob.py | 89 +++++++++++++++++++++++ 3 files changed, 106 insertions(+), 63 deletions(-) create mode 100644 plugins/CuraDrive/src/RestoreBackupJob.py diff --git a/plugins/CuraDrive/src/CreateBackupJob.py b/plugins/CuraDrive/src/CreateBackupJob.py index 603733137b..4a5764a3b6 100644 --- a/plugins/CuraDrive/src/CreateBackupJob.py +++ b/plugins/CuraDrive/src/CreateBackupJob.py @@ -36,10 +36,8 @@ class CreateBackupJob(Job): 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._job_done = threading.Event() self.backup_upload_error_message = "" def run(self) -> None: @@ -62,7 +60,7 @@ class CreateBackupJob(Job): 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() + self._job_done.wait() upload_message.hide() def _requestUploadSlot(self, backup_metadata: Dict[str, Any], backup_size: int) -> None: @@ -89,12 +87,12 @@ class CreateBackupJob(Job): if error is not None: Logger.warning(str(error)) self.backup_upload_error_message = "Could not upload backup." - self._upload_success_available.set() + self._job_done.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() + self._job_done.set() return backup_upload_url = HttpRequestManager.readJSON(reply)["data"]["upload_url"] @@ -119,4 +117,4 @@ class CreateBackupJob(Job): Message(catalog.i18nc("@info:backup_status", "There was an error while uploading your backup."), title=self.MESSAGE_TITLE).show() - self._upload_success_available.set() + self._job_done.set() diff --git a/plugins/CuraDrive/src/DriveApiService.py b/plugins/CuraDrive/src/DriveApiService.py index 04f935268b..3af461126a 100644 --- a/plugins/CuraDrive/src/DriveApiService.py +++ b/plugins/CuraDrive/src/DriveApiService.py @@ -11,6 +11,7 @@ from UM.Signal import Signal, signalemitter from UM.TaskManagement.HttpRequestManager import HttpRequestManager from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope from cura.CuraApplication import CuraApplication +from plugins.CuraDrive.src.RestoreBackupJob import RestoreBackupJob from plugins.Toolbox.src.UltimakerCloudScope import UltimakerCloudScope from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest @@ -27,7 +28,6 @@ class DriveApiService: """The DriveApiService is responsible for interacting with the CuraDrive API and Cura's backup handling.""" BACKUP_URL = "{}/backups".format(Settings.DRIVE_API_URL) - DISK_WRITE_BUFFER_SIZE = 512 * 1024 restoringStateChanged = Signal() """Emits signal when restoring backup started or finished.""" @@ -82,61 +82,16 @@ class DriveApiService: # If there is no download URL, we can't restore the backup. return self._emitRestoreError() - def finishedCallback(reply: QNetworkReply, bu=backup) -> None: - self._onRestoreRequestCompleted(reply, None, bu) + restore_backup_job = RestoreBackupJob(backup) + restore_backup_job.finished.connect(self._onRestoreFinished) + restore_backup_job.start() - HttpRequestManager.getInstance().get( - url = download_url, - callback = finishedCallback, - error_callback = self._onRestoreRequestCompleted - ) - - def _onRestoreRequestCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None, backup = None): - if not HttpRequestManager.replyIndicatesSuccess(reply, error): - Logger.log("w", - "Requesting backup failed, response code %s while trying to connect to %s", - reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url()) - self._emitRestoreError() - return - - # We store the file in a temporary path fist to ensure integrity. - temporary_backup_file = NamedTemporaryFile(delete = False) - with open(temporary_backup_file.name, "wb") as write_backup: - app = CuraApplication.getInstance() - bytes_read = reply.read(DriveApiService.DISK_WRITE_BUFFER_SIZE) - while bytes_read: - write_backup.write(bytes_read) - bytes_read = reply.read(DriveApiService.DISK_WRITE_BUFFER_SIZE) - app.processEvents() - - if not self._verifyMd5Hash(temporary_backup_file.name, backup.get("md5_hash", "")): - # Don't restore the backup if the MD5 hashes do not match. - # This can happen if the download was interrupted. - Logger.log("w", "Remote and local MD5 hashes do not match, not restoring backup.") - return self._emitRestoreError() - - # Tell Cura to place the backup back in the user data folder. - with open(temporary_backup_file.name, "rb") as read_backup: - self._cura_api.backups.restoreBackup(read_backup.read(), backup.get("metadata", {})) - self.restoringStateChanged.emit(is_restoring = False) - - def _emitRestoreError(self) -> None: - self.restoringStateChanged.emit(is_restoring = False, - error_message = catalog.i18nc("@info:backup_status", - "There was an error trying to restore your backup.")) - - @staticmethod - def _verifyMd5Hash(file_path: str, known_hash: str) -> bool: - """Verify the MD5 hash of a file. - - :param file_path: Full path to the file. - :param known_hash: The known MD5 hash of the file. - :return: Success or not. - """ - - with open(file_path, "rb") as read_backup: - local_md5_hash = base64.b64encode(hashlib.md5(read_backup.read()).digest(), altchars = b"_-").decode("utf-8") - return known_hash == local_md5_hash + def _onRestoreFinished(self, job: "RestoreBackupJob"): + if job.restore_backup_error_message != "": + # If the job contains an error message we pass it along so the UI can display it. + self.restoringStateChanged.emit(is_restoring=False) + else: + self.restoringStateChanged.emit(is_restoring = False, error_message = job.restore_backup_error_message) def deleteBackup(self, backup_id: str, finishedCallable: Callable[[bool], None]): @@ -153,5 +108,6 @@ class DriveApiService: scope= self._jsonCloudScope ) - def _onDeleteRequestCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None, callable = None): + @staticmethod + def _onDeleteRequestCompleted(reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None, callable = None): callable(HttpRequestManager.replyIndicatesSuccess(reply, error)) diff --git a/plugins/CuraDrive/src/RestoreBackupJob.py b/plugins/CuraDrive/src/RestoreBackupJob.py new file mode 100644 index 0000000000..86e32aca97 --- /dev/null +++ b/plugins/CuraDrive/src/RestoreBackupJob.py @@ -0,0 +1,89 @@ +import base64 +import hashlib +import threading +from tempfile import NamedTemporaryFile +from typing import Optional, Any, Dict + +from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest + +from UM.Job import Job +from UM.Logger import Logger +from UM.PackageManager import catalog +from UM.TaskManagement.HttpRequestManager import HttpRequestManager +from cura.CuraApplication import CuraApplication + + +class RestoreBackupJob(Job): + """Downloads a backup and overwrites local configuration with the backup. + + When `Job.finished` emits, `restore_backup_error_message` will either be `""` (no error) or an error message + """ + + DISK_WRITE_BUFFER_SIZE = 512 * 1024 + DEFAULT_ERROR_MESSAGE = catalog.i18nc("@info:backup_status", "There was an error trying to restore your backup.") + + def __init__(self, backup: Dict[str, Any]) -> None: + """ Create a new restore Job. start the job by calling start() + + :param backup: A dict containing a backup spec + """ + + super().__init__() + self._job_done = threading.Event() + + self._backup = backup + self.restore_backup_error_message = "" + + def run(self): + + HttpRequestManager.getInstance().get( + url = self._backup.get("download_url"), + callback = self._onRestoreRequestCompleted, + error_callback = self._onRestoreRequestCompleted + ) + + self._job_done.wait() + + def _onRestoreRequestCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None): + if not HttpRequestManager.replyIndicatesSuccess(reply, error): + Logger.warning("Requesting backup failed, response code %s while trying to connect to %s", + reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url()) + self.restore_backup_error_message = self.DEFAULT_ERROR_MESSAGE + self._job_done.set() + return + + # We store the file in a temporary path fist to ensure integrity. + temporary_backup_file = NamedTemporaryFile(delete = False) + with open(temporary_backup_file.name, "wb") as write_backup: + app = CuraApplication.getInstance() + bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) + while bytes_read: + write_backup.write(bytes_read) + bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) + app.processEvents() + + if not self._verifyMd5Hash(temporary_backup_file.name, self._backup.get("md5_hash", "")): + # Don't restore the backup if the MD5 hashes do not match. + # This can happen if the download was interrupted. + Logger.log("w", "Remote and local MD5 hashes do not match, not restoring backup.") + self.restore_backup_error_message = self.DEFAULT_ERROR_MESSAGE + + # Tell Cura to place the backup back in the user data folder. + with open(temporary_backup_file.name, "rb") as read_backup: + cura_api = CuraApplication.getInstance().getCuraAPI() + cura_api.backups.restoreBackup(read_backup.read(), self._backup.get("metadata", {})) + + self._job_done.set() + + @staticmethod + def _verifyMd5Hash(file_path: str, known_hash: str) -> bool: + """Verify the MD5 hash of a file. + + :param file_path: Full path to the file. + :param known_hash: The known MD5 hash of the file. + :return: Success or not. + """ + + with open(file_path, "rb") as read_backup: + local_md5_hash = base64.b64encode(hashlib.md5(read_backup.read()).digest(), altchars = b"_-").decode("utf-8") + return known_hash == local_md5_hash From d11b3b0921edb708e7ce48d647dc8f35a1f385fc Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Tue, 10 Mar 2020 14:23:40 +0100 Subject: [PATCH 14/22] Extra comment for RestoreBackupJob --- plugins/CuraDrive/src/RestoreBackupJob.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/CuraDrive/src/RestoreBackupJob.py b/plugins/CuraDrive/src/RestoreBackupJob.py index 86e32aca97..08f9d73e1e 100644 --- a/plugins/CuraDrive/src/RestoreBackupJob.py +++ b/plugins/CuraDrive/src/RestoreBackupJob.py @@ -42,7 +42,7 @@ class RestoreBackupJob(Job): error_callback = self._onRestoreRequestCompleted ) - self._job_done.wait() + self._job_done.wait() # A job is considered finished when the run function completes def _onRestoreRequestCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None): if not HttpRequestManager.replyIndicatesSuccess(reply, error): From 61af28c681982682ea3905be4798f38ca5c949ba Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Thu, 12 Mar 2020 13:57:09 +0100 Subject: [PATCH 15/22] Process review comments. CURA-7150 --- plugins/Toolbox/src/UltimakerCloudScope.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/Toolbox/src/UltimakerCloudScope.py b/plugins/Toolbox/src/UltimakerCloudScope.py index 257e97b1af..0e9adaf2e7 100644 --- a/plugins/Toolbox/src/UltimakerCloudScope.py +++ b/plugins/Toolbox/src/UltimakerCloudScope.py @@ -18,8 +18,8 @@ class UltimakerCloudScope(DefaultUserAgentScope): api = application.getCuraAPI() self._account = api.account # type: Account - def request_hook(self, request: QNetworkRequest): - super().request_hook(request) + def requestHook(self, request: QNetworkRequest): + super().requestHook(request) token = self._account.accessToken if not self._account.isLoggedIn or token is None: Logger.warning("Cannot add authorization to Cloud Api request") @@ -28,4 +28,4 @@ class UltimakerCloudScope(DefaultUserAgentScope): header_dict = { "Authorization": "Bearer {}".format(token) } - self.add_headers(request, header_dict) + self.addHeaders(request, header_dict) From 6dd8ebb06a41839793754acd438cbbdda1d3ef78 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Thu, 12 Mar 2020 17:06:33 +0100 Subject: [PATCH 16/22] Process review comments in Cura project CURA-7150 --- plugins/CuraDrive/src/CreateBackupJob.py | 40 +++++++++++------------ plugins/CuraDrive/src/DriveApiService.py | 20 ++++++------ plugins/CuraDrive/src/RestoreBackupJob.py | 4 +-- 3 files changed, 31 insertions(+), 33 deletions(-) diff --git a/plugins/CuraDrive/src/CreateBackupJob.py b/plugins/CuraDrive/src/CreateBackupJob.py index 4a5764a3b6..8e9bd9ad4c 100644 --- a/plugins/CuraDrive/src/CreateBackupJob.py +++ b/plugins/CuraDrive/src/CreateBackupJob.py @@ -1,18 +1,17 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2020 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import json import threading +from datetime import datetime 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 @@ -24,6 +23,7 @@ class CreateBackupJob(Job): """Creates backup zip, requests upload url and uploads the backup file to cloud storage.""" MESSAGE_TITLE = catalog.i18nc("@info:title", "Backups") + DEFAULT_UPLOAD_ERROR_MESSAGE = catalog.i18nc("@info:backup_status", "There was an error while uploading your backup.") def __init__(self, api_backup_url: str) -> None: """ Create a new backup Job. start the job by calling start() @@ -34,11 +34,13 @@ class CreateBackupJob(Job): super().__init__() self._api_backup_url = api_backup_url - self._jsonCloudScope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) + self._json_cloud_scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) - self._backup_zip = None + self._backup_zip = None # type: Optional[bytes] self._job_done = threading.Event() + """Set when the job completes. Does not indicate success.""" self.backup_upload_error_message = "" + """After the job completes, an empty string indicates success. Othrerwise, the value is a translated message.""" def run(self) -> None: upload_message = Message(catalog.i18nc("@info:backup_status", "Creating your backup..."), title = self.MESSAGE_TITLE, progress = -1) @@ -48,7 +50,7 @@ class CreateBackupJob(Job): 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." + self.backup_upload_error_message = catalog.i18nc("@info:backup_status", "There was an error while creating your backup.") upload_message.hide() return @@ -61,14 +63,17 @@ class CreateBackupJob(Job): self._requestUploadSlot(backup_meta_data, len(self._backup_zip)) self._job_done.wait() - upload_message.hide() + if self.backup_upload_error_message == "": + upload_message.setText(catalog.i18nc("@info:backup_status", "Your backup has finished uploading.")) + else: + # some error occurred. This error is presented to the user by DrivePluginExtension + 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, @@ -81,17 +86,17 @@ class CreateBackupJob(Job): data = payload, callback = self._onUploadSlotCompleted, error_callback = self._onUploadSlotCompleted, - scope = self._jsonCloudScope) + scope = self._json_cloud_scope) 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.backup_upload_error_message = self.DEFAULT_UPLOAD_ERROR_MESSAGE self._job_done.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.backup_upload_error_message = self.DEFAULT_UPLOAD_ERROR_MESSAGE self._job_done.set() return @@ -106,15 +111,8 @@ class CreateBackupJob(Job): ) 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() + if not HttpRequestManager.replyIndicatesSuccess(reply, error): + Logger.log("w", "Could not upload backup file: %s", HttpRequestManager.readText(reply)) + self.backup_upload_error_message = self.DEFAULT_UPLOAD_ERROR_MESSAGE self._job_done.set() diff --git a/plugins/CuraDrive/src/DriveApiService.py b/plugins/CuraDrive/src/DriveApiService.py index 3af461126a..0fe89447e6 100644 --- a/plugins/CuraDrive/src/DriveApiService.py +++ b/plugins/CuraDrive/src/DriveApiService.py @@ -1,25 +1,21 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -import base64 -import hashlib -from tempfile import NamedTemporaryFile from typing import Any, Optional, List, Dict, Callable +from PyQt5.QtNetwork import QNetworkReply + from UM.Logger import Logger from UM.Signal import Signal, signalemitter from UM.TaskManagement.HttpRequestManager import HttpRequestManager from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope +from UM.i18n import i18nCatalog from cura.CuraApplication import CuraApplication from plugins.CuraDrive.src.RestoreBackupJob import RestoreBackupJob from plugins.Toolbox.src.UltimakerCloudScope import UltimakerCloudScope - -from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest - from .CreateBackupJob import CreateBackupJob from .Settings import Settings -from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") @@ -39,8 +35,8 @@ class DriveApiService: self._cura_api = CuraApplication.getInstance().getCuraAPI() self._jsonCloudScope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) - def getBackups(self, changed: Callable[[List], None]): - def callback(reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None): + def getBackups(self, changed: Callable[[List[Dict[str, Any]]], None]) -> None: + def callback(reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None) -> None: if error is not None: Logger.log("w", "Could not get backups: " + str(error)) changed([]) @@ -80,7 +76,11 @@ class DriveApiService: download_url = backup.get("download_url") if not download_url: # If there is no download URL, we can't restore the backup. - return self._emitRestoreError() + Logger.warning("backup download_url is missing. Aborting backup.") + self.restoringStateChanged.emit(is_restoring = False, + error_message = catalog.i18nc("@info:backup_status", + "There was an error trying to restore your backup.")) + return restore_backup_job = RestoreBackupJob(backup) restore_backup_job.finished.connect(self._onRestoreFinished) diff --git a/plugins/CuraDrive/src/RestoreBackupJob.py b/plugins/CuraDrive/src/RestoreBackupJob.py index 08f9d73e1e..e26e55fd37 100644 --- a/plugins/CuraDrive/src/RestoreBackupJob.py +++ b/plugins/CuraDrive/src/RestoreBackupJob.py @@ -34,7 +34,7 @@ class RestoreBackupJob(Job): self._backup = backup self.restore_backup_error_message = "" - def run(self): + def run(self) -> None: HttpRequestManager.getInstance().get( url = self._backup.get("download_url"), @@ -44,7 +44,7 @@ class RestoreBackupJob(Job): self._job_done.wait() # A job is considered finished when the run function completes - def _onRestoreRequestCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None): + def _onRestoreRequestCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None) -> None: if not HttpRequestManager.replyIndicatesSuccess(reply, error): Logger.warning("Requesting backup failed, response code %s while trying to connect to %s", reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url()) From 2e7b47f1ea8f6a7efb46266722d9c50e1c47150a Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Thu, 12 Mar 2020 17:21:03 +0100 Subject: [PATCH 17/22] Add None-check for download url CURA-7150 --- plugins/CuraDrive/src/RestoreBackupJob.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/CuraDrive/src/RestoreBackupJob.py b/plugins/CuraDrive/src/RestoreBackupJob.py index e26e55fd37..33ca115c06 100644 --- a/plugins/CuraDrive/src/RestoreBackupJob.py +++ b/plugins/CuraDrive/src/RestoreBackupJob.py @@ -36,8 +36,11 @@ class RestoreBackupJob(Job): def run(self) -> None: + url = self._backup.get("download_url") + assert url is not None + HttpRequestManager.getInstance().get( - url = self._backup.get("download_url"), + url =url, callback = self._onRestoreRequestCompleted, error_callback = self._onRestoreRequestCompleted ) From 96ed85f9c0c193deeb5637a4d9dd6d36053fa1f0 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Thu, 12 Mar 2020 17:48:54 +0100 Subject: [PATCH 18/22] Move UltimakerCloudAuthentication and UltimakerCloudScope ...to their own module Fixes an import error where UltimakerCloudScope was used my both the Toolbox and CuraDrive plugins CURA-7150 --- cura/API/Account.py | 5 +- cura/CuraApplication.py | 85 +++++++------------ .../UltimakerCloudAuthentication.py | 0 .../UltimakerCloud}/UltimakerCloudScope.py | 0 cura/UltimakerCloud/__init__.py | 0 plugins/CuraDrive/src/CreateBackupJob.py | 2 +- plugins/CuraDrive/src/DriveApiService.py | 4 +- plugins/CuraDrive/src/RestoreBackupJob.py | 2 +- plugins/CuraDrive/src/Settings.py | 2 +- plugins/Toolbox/src/CloudApiModel.py | 3 +- .../Toolbox/src/CloudSync/CloudApiClient.py | 2 +- .../src/CloudSync/CloudPackageChecker.py | 7 +- .../src/CloudSync/DownloadPresenter.py | 2 +- plugins/Toolbox/src/Toolbox.py | 11 +-- .../src/Cloud/CloudApiClient.py | 8 +- 15 files changed, 53 insertions(+), 80 deletions(-) rename cura/{ => UltimakerCloud}/UltimakerCloudAuthentication.py (100%) rename {plugins/Toolbox/src => cura/UltimakerCloud}/UltimakerCloudScope.py (100%) create mode 100644 cura/UltimakerCloud/__init__.py diff --git a/cura/API/Account.py b/cura/API/Account.py index 0e3af0e6c1..4391f730e5 100644 --- a/cura/API/Account.py +++ b/cura/API/Account.py @@ -4,12 +4,11 @@ from typing import Optional, Dict, TYPE_CHECKING from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty -from UM.i18n import i18nCatalog from UM.Message import Message -from cura import UltimakerCloudAuthentication - +from UM.i18n import i18nCatalog from cura.OAuth2.AuthorizationService import AuthorizationService from cura.OAuth2.Models import OAuth2Settings +from cura.UltimakerCloud import UltimakerCloudAuthentication if TYPE_CHECKING: from cura.CuraApplication import CuraApplication diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 7b17583f68..e58e03bf67 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -7,71 +7,52 @@ import time from typing import cast, TYPE_CHECKING, Optional, Callable, List, Any import numpy - from PyQt5.QtCore import QObject, QTimer, QUrl, pyqtSignal, pyqtProperty, QEvent, Q_ENUMS from PyQt5.QtGui import QColor, QIcon -from PyQt5.QtWidgets import QMessageBox from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType +from PyQt5.QtWidgets import QMessageBox -from UM.i18n import i18nCatalog +import UM.Util +import cura.Settings.cura_empty_instance_containers from UM.Application import Application from UM.Decorators import override from UM.FlameProfiler import pyqtSlot from UM.Logger import Logger -from UM.Message import Message -from UM.Platform import Platform -from UM.PluginError import PluginNotFoundError -from UM.Resources import Resources -from UM.Preferences import Preferences -from UM.Qt.QtApplication import QtApplication # The class we're inheriting from. -import UM.Util -from UM.View.SelectionPass import SelectionPass # For typing. - from UM.Math.AxisAlignedBox import AxisAlignedBox from UM.Math.Matrix import Matrix from UM.Math.Quaternion import Quaternion from UM.Math.Vector import Vector - from UM.Mesh.ReadMeshJob import ReadMeshJob - +from UM.Message import Message from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.SetTransformOperation import SetTransformOperation - +from UM.Platform import Platform +from UM.PluginError import PluginNotFoundError +from UM.Preferences import Preferences +from UM.Qt.QtApplication import QtApplication # The class we're inheriting from. +from UM.Resources import Resources from UM.Scene.Camera import Camera from UM.Scene.GroupDecorator import GroupDecorator from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.SceneNode import SceneNode from UM.Scene.Selection import Selection from UM.Scene.ToolHandle import ToolHandle - from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType from UM.Settings.SettingFunction import SettingFunction from UM.Settings.Validator import Validator - +from UM.View.SelectionPass import SelectionPass # For typing. from UM.Workspace.WorkspaceReader import WorkspaceReader - +from UM.i18n import i18nCatalog +from cura import ApplicationMetadata from cura.API import CuraAPI - from cura.Arranging.Arrange import Arrange -from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob +from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob from cura.Arranging.ShapeArray import ShapeArray - -from cura.Operations.SetParentOperation import SetParentOperation - -from cura.Scene.BlockSlicingDecorator import BlockSlicingDecorator -from cura.Scene.BuildPlateDecorator import BuildPlateDecorator -from cura.Scene.ConvexHullDecorator import ConvexHullDecorator -from cura.Scene.CuraSceneController import CuraSceneController -from cura.Scene.CuraSceneNode import CuraSceneNode - -from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator -from cura.Scene import ZOffsetDecorator from cura.Machines.MachineErrorChecker import MachineErrorChecker - from cura.Machines.Models.BuildPlateModel import BuildPlateModel from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel from cura.Machines.Models.DiscoveredPrintersModel import DiscoveredPrintersModel @@ -80,6 +61,8 @@ from cura.Machines.Models.FavoriteMaterialsModel import FavoriteMaterialsModel from cura.Machines.Models.FirstStartMachineActionsModel import FirstStartMachineActionsModel from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel from cura.Machines.Models.GlobalStacksModel import GlobalStacksModel +from cura.Machines.Models.IntentCategoryModel import IntentCategoryModel +from cura.Machines.Models.IntentModel import IntentModel from cura.Machines.Models.MaterialBrandsModel import MaterialBrandsModel from cura.Machines.Models.MaterialManagementModel import MaterialManagementModel from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel @@ -89,51 +72,47 @@ from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfile from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel from cura.Machines.Models.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel from cura.Machines.Models.UserChangesModel import UserChangesModel -from cura.Machines.Models.IntentModel import IntentModel -from cura.Machines.Models.IntentCategoryModel import IntentCategoryModel - -from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice +from cura.Operations.SetParentOperation import SetParentOperation from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage - -import cura.Settings.cura_empty_instance_containers +from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice +from cura.Scene import ZOffsetDecorator +from cura.Scene.BlockSlicingDecorator import BlockSlicingDecorator +from cura.Scene.BuildPlateDecorator import BuildPlateDecorator +from cura.Scene.ConvexHullDecorator import ConvexHullDecorator +from cura.Scene.CuraSceneController import CuraSceneController +from cura.Scene.CuraSceneNode import CuraSceneNode +from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator from cura.Settings.ContainerManager import ContainerManager from cura.Settings.CuraContainerRegistry import CuraContainerRegistry from cura.Settings.CuraFormulaFunctions import CuraFormulaFunctions from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderStack import ExtruderStack +from cura.Settings.GlobalStack import GlobalStack +from cura.Settings.IntentManager import IntentManager from cura.Settings.MachineManager import MachineManager from cura.Settings.MachineNameValidator import MachineNameValidator -from cura.Settings.IntentManager import IntentManager from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler from cura.Settings.SettingInheritanceManager import SettingInheritanceManager from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager - from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager - from cura.UI import CuraSplashScreen, MachineActionManager, PrintInformation +from cura.UI.AddPrinterPagesModel import AddPrinterPagesModel from cura.UI.MachineSettingsManager import MachineSettingsManager from cura.UI.ObjectsModel import ObjectsModel -from cura.UI.TextManager import TextManager -from cura.UI.AddPrinterPagesModel import AddPrinterPagesModel from cura.UI.RecommendedMode import RecommendedMode +from cura.UI.TextManager import TextManager from cura.UI.WelcomePagesModel import WelcomePagesModel from cura.UI.WhatsNewPagesModel import WhatsNewPagesModel - +from cura.UltimakerCloud import UltimakerCloudAuthentication from cura.Utils.NetworkingUtil import NetworkingUtil - -from .SingleInstance import SingleInstance -from .AutoSave import AutoSave -from . import PlatformPhysics from . import BuildVolume from . import CameraAnimation from . import CuraActions +from . import PlatformPhysics from . import PrintJobPreviewImageProvider - -from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager - -from cura import ApplicationMetadata, UltimakerCloudAuthentication -from cura.Settings.GlobalStack import GlobalStack +from .AutoSave import AutoSave +from .SingleInstance import SingleInstance if TYPE_CHECKING: from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer diff --git a/cura/UltimakerCloudAuthentication.py b/cura/UltimakerCloud/UltimakerCloudAuthentication.py similarity index 100% rename from cura/UltimakerCloudAuthentication.py rename to cura/UltimakerCloud/UltimakerCloudAuthentication.py diff --git a/plugins/Toolbox/src/UltimakerCloudScope.py b/cura/UltimakerCloud/UltimakerCloudScope.py similarity index 100% rename from plugins/Toolbox/src/UltimakerCloudScope.py rename to cura/UltimakerCloud/UltimakerCloudScope.py diff --git a/cura/UltimakerCloud/__init__.py b/cura/UltimakerCloud/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/CuraDrive/src/CreateBackupJob.py b/plugins/CuraDrive/src/CreateBackupJob.py index 8e9bd9ad4c..f5f557d003 100644 --- a/plugins/CuraDrive/src/CreateBackupJob.py +++ b/plugins/CuraDrive/src/CreateBackupJob.py @@ -14,7 +14,7 @@ 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 +from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope catalog = i18nCatalog("cura") diff --git a/plugins/CuraDrive/src/DriveApiService.py b/plugins/CuraDrive/src/DriveApiService.py index 0fe89447e6..fa010d915d 100644 --- a/plugins/CuraDrive/src/DriveApiService.py +++ b/plugins/CuraDrive/src/DriveApiService.py @@ -11,9 +11,9 @@ from UM.TaskManagement.HttpRequestManager import HttpRequestManager from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope from UM.i18n import i18nCatalog from cura.CuraApplication import CuraApplication -from plugins.CuraDrive.src.RestoreBackupJob import RestoreBackupJob -from plugins.Toolbox.src.UltimakerCloudScope import UltimakerCloudScope +from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope from .CreateBackupJob import CreateBackupJob +from .RestoreBackupJob import RestoreBackupJob from .Settings import Settings catalog = i18nCatalog("cura") diff --git a/plugins/CuraDrive/src/RestoreBackupJob.py b/plugins/CuraDrive/src/RestoreBackupJob.py index 33ca115c06..c60de116e0 100644 --- a/plugins/CuraDrive/src/RestoreBackupJob.py +++ b/plugins/CuraDrive/src/RestoreBackupJob.py @@ -40,7 +40,7 @@ class RestoreBackupJob(Job): assert url is not None HttpRequestManager.getInstance().get( - url =url, + url = url, callback = self._onRestoreRequestCompleted, error_callback = self._onRestoreRequestCompleted ) diff --git a/plugins/CuraDrive/src/Settings.py b/plugins/CuraDrive/src/Settings.py index abe64e0acd..639c63b45f 100644 --- a/plugins/CuraDrive/src/Settings.py +++ b/plugins/CuraDrive/src/Settings.py @@ -1,7 +1,7 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from cura import UltimakerCloudAuthentication +from cura.UltimakerCloud import UltimakerCloudAuthentication class Settings: diff --git a/plugins/Toolbox/src/CloudApiModel.py b/plugins/Toolbox/src/CloudApiModel.py index 556d54cf88..3386cffb51 100644 --- a/plugins/Toolbox/src/CloudApiModel.py +++ b/plugins/Toolbox/src/CloudApiModel.py @@ -1,6 +1,7 @@ from typing import Union -from cura import ApplicationMetadata, UltimakerCloudAuthentication +from cura import ApplicationMetadata +from cura.UltimakerCloud import UltimakerCloudAuthentication class CloudApiModel: diff --git a/plugins/Toolbox/src/CloudSync/CloudApiClient.py b/plugins/Toolbox/src/CloudSync/CloudApiClient.py index ba1cbab624..21eb1bdbd2 100644 --- a/plugins/Toolbox/src/CloudSync/CloudApiClient.py +++ b/plugins/Toolbox/src/CloudSync/CloudApiClient.py @@ -2,8 +2,8 @@ from UM.Logger import Logger from UM.TaskManagement.HttpRequestManager import HttpRequestManager from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope from cura.CuraApplication import CuraApplication +from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope from ..CloudApiModel import CloudApiModel -from ..UltimakerCloudScope import UltimakerCloudScope class CloudApiClient: diff --git a/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py b/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py index 7404489aee..76c9c092c5 100644 --- a/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py +++ b/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py @@ -2,6 +2,7 @@ # Cura is released under the terms of the LGPLv3 or higher. import json +from typing import List, Dict, Any from typing import Optional from PyQt5.QtCore import QObject @@ -13,11 +14,9 @@ from UM.Message import Message from UM.Signal import Signal from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope from cura.CuraApplication import CuraApplication, ApplicationMetadata -from ..CloudApiModel import CloudApiModel +from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope from .SubscribedPackagesModel import SubscribedPackagesModel -from ..UltimakerCloudScope import UltimakerCloudScope - -from typing import List, Dict, Any +from ..CloudApiModel import CloudApiModel class CloudPackageChecker(QObject): diff --git a/plugins/Toolbox/src/CloudSync/DownloadPresenter.py b/plugins/Toolbox/src/CloudSync/DownloadPresenter.py index 06004df5da..a16ed67218 100644 --- a/plugins/Toolbox/src/CloudSync/DownloadPresenter.py +++ b/plugins/Toolbox/src/CloudSync/DownloadPresenter.py @@ -13,8 +13,8 @@ from UM.Signal import Signal from UM.TaskManagement.HttpRequestManager import HttpRequestManager from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope from cura.CuraApplication import CuraApplication +from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope from .SubscribedPackagesModel import SubscribedPackagesModel -from ..UltimakerCloudScope import UltimakerCloudScope ## Downloads a set of packages from the Ultimaker Cloud Marketplace diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index 9057538835..ec1da6707a 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -9,23 +9,20 @@ from typing import cast, Any, Dict, List, Set, TYPE_CHECKING, Tuple, Optional, U from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply +from UM.Extension import Extension from UM.Logger import Logger from UM.PluginRegistry import PluginRegistry -from UM.Extension import Extension from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope -from UM.i18n import i18nCatalog from UM.Version import Version - +from UM.i18n import i18nCatalog from cura import ApplicationMetadata - from cura.CuraApplication import CuraApplication from cura.Machines.ContainerTree import ContainerTree - -from .CloudApiModel import CloudApiModel +from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope from .AuthorsModel import AuthorsModel +from .CloudApiModel import CloudApiModel from .CloudSync.LicenseModel import LicenseModel from .PackagesModel import PackagesModel -from .UltimakerCloudScope import UltimakerCloudScope if TYPE_CHECKING: from UM.TaskManagement.HttpRequestData import HttpRequestData diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py index ed8d22a478..6fec436843 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py @@ -9,18 +9,16 @@ from PyQt5.QtCore import QUrl from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager from UM.Logger import Logger -from cura import UltimakerCloudAuthentication from cura.API import Account - +from cura.UltimakerCloud import UltimakerCloudAuthentication from .ToolPathUploader import ToolPathUploader from ..Models.BaseModel import BaseModel from ..Models.Http.CloudClusterResponse import CloudClusterResponse -from ..Models.Http.CloudError import CloudError from ..Models.Http.CloudClusterStatus import CloudClusterStatus +from ..Models.Http.CloudError import CloudError +from ..Models.Http.CloudPrintJobResponse import CloudPrintJobResponse from ..Models.Http.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest from ..Models.Http.CloudPrintResponse import CloudPrintResponse -from ..Models.Http.CloudPrintJobResponse import CloudPrintJobResponse - ## The generic type variable used to document the methods below. CloudApiClientModel = TypeVar("CloudApiClientModel", bound=BaseModel) From 4e8da5d7fd9585a5b7cd346206abd4a6749724cd Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 16 Mar 2020 17:20:59 +0100 Subject: [PATCH 19/22] Add missing typing CURA-7150 --- plugins/CuraDrive/src/DriveApiService.py | 14 +++++++------- plugins/CuraDrive/src/DrivePluginExtension.py | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/CuraDrive/src/DriveApiService.py b/plugins/CuraDrive/src/DriveApiService.py index fa010d915d..367c6e61a5 100644 --- a/plugins/CuraDrive/src/DriveApiService.py +++ b/plugins/CuraDrive/src/DriveApiService.py @@ -86,20 +86,20 @@ class DriveApiService: restore_backup_job.finished.connect(self._onRestoreFinished) restore_backup_job.start() - def _onRestoreFinished(self, job: "RestoreBackupJob"): + def _onRestoreFinished(self, job: "RestoreBackupJob") -> None: if job.restore_backup_error_message != "": # If the job contains an error message we pass it along so the UI can display it. self.restoringStateChanged.emit(is_restoring=False) else: self.restoringStateChanged.emit(is_restoring = False, error_message = job.restore_backup_error_message) - def deleteBackup(self, backup_id: str, finishedCallable: Callable[[bool], None]): + def deleteBackup(self, backup_id: str, finished_callable: Callable[[bool], None]): - def finishedCallback(reply: QNetworkReply, ca=finishedCallable) -> None: - self._onDeleteRequestCompleted(reply, None, ca) + def finishedCallback(reply: QNetworkReply, ca: Callable[[bool], None] = finished_callable) -> None: + self._onDeleteRequestCompleted(reply, ca) - def errorCallback(reply: QNetworkReply, error: QNetworkReply.NetworkError, ca=finishedCallable) -> None: - self._onDeleteRequestCompleted(reply, error, ca) + def errorCallback(reply: QNetworkReply, error: QNetworkReply.NetworkError, ca: Callable[[bool], None] = finished_callable) -> None: + self._onDeleteRequestCompleted(reply, ca, error) HttpRequestManager.getInstance().delete( url = "{}/{}".format(self.BACKUP_URL, backup_id), @@ -109,5 +109,5 @@ class DriveApiService: ) @staticmethod - def _onDeleteRequestCompleted(reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None, callable = None): + def _onDeleteRequestCompleted(reply: QNetworkReply, callable: Callable[[bool], None], error: Optional["QNetworkReply.NetworkError"] = None) -> None: callable(HttpRequestManager.replyIndicatesSuccess(reply, error)) diff --git a/plugins/CuraDrive/src/DrivePluginExtension.py b/plugins/CuraDrive/src/DrivePluginExtension.py index 34f30d8b3d..8de4876f52 100644 --- a/plugins/CuraDrive/src/DrivePluginExtension.py +++ b/plugins/CuraDrive/src/DrivePluginExtension.py @@ -135,7 +135,7 @@ class DrivePluginExtension(QObject, Extension): def refreshBackups(self) -> None: self._drive_api_service.getBackups(self._backupsChangedCallback) - def _backupsChangedCallback(self, backups): + def _backupsChangedCallback(self, backups: List[Dict[str, Any]]) -> None: self._backups = backups self.backupsChanged.emit() From 72a66f1358e99d634bb124d5162ea8499842437c Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Fri, 20 Mar 2020 17:31:21 +0100 Subject: [PATCH 20/22] Do not use the JsonDecorator scope for binary downloads CURA-7150 --- plugins/CuraDrive/src/DriveApiService.py | 6 +++--- plugins/Toolbox/src/CloudSync/DownloadPresenter.py | 3 +-- plugins/Toolbox/src/Toolbox.py | 9 +++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/plugins/CuraDrive/src/DriveApiService.py b/plugins/CuraDrive/src/DriveApiService.py index fa010d915d..0c8d38ebd2 100644 --- a/plugins/CuraDrive/src/DriveApiService.py +++ b/plugins/CuraDrive/src/DriveApiService.py @@ -33,7 +33,7 @@ class DriveApiService: def __init__(self) -> None: self._cura_api = CuraApplication.getInstance().getCuraAPI() - self._jsonCloudScope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) + self._json_cloud_scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) def getBackups(self, changed: Callable[[List[Dict[str, Any]]], None]) -> None: def callback(reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None) -> None: @@ -55,7 +55,7 @@ class DriveApiService: self.BACKUP_URL, callback= callback, error_callback = callback, - scope=self._jsonCloudScope + scope=self._json_cloud_scope ) def createBackup(self) -> None: @@ -105,7 +105,7 @@ class DriveApiService: url = "{}/{}".format(self.BACKUP_URL, backup_id), callback = finishedCallback, error_callback = errorCallback, - scope= self._jsonCloudScope + scope= self._json_cloud_scope ) @staticmethod diff --git a/plugins/Toolbox/src/CloudSync/DownloadPresenter.py b/plugins/Toolbox/src/CloudSync/DownloadPresenter.py index a16ed67218..a5d6eee0b6 100644 --- a/plugins/Toolbox/src/CloudSync/DownloadPresenter.py +++ b/plugins/Toolbox/src/CloudSync/DownloadPresenter.py @@ -11,7 +11,6 @@ from UM.Logger import Logger from UM.Message import Message from UM.Signal import Signal from UM.TaskManagement.HttpRequestManager import HttpRequestManager -from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope from cura.CuraApplication import CuraApplication from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope from .SubscribedPackagesModel import SubscribedPackagesModel @@ -30,7 +29,7 @@ class DownloadPresenter: self.done = Signal() self._app = app - self._scope = JsonDecoratorScope(UltimakerCloudScope(app)) + self._scope = UltimakerCloudScope(app) self._started = False self._progress_message = self._createProgressMessage() diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index ec1da6707a..38666bb6e2 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -52,7 +52,8 @@ class Toolbox(QObject, Extension): self._download_request_data = None # type: Optional[HttpRequestData] self._download_progress = 0 # type: float self._is_downloading = False # type: bool - self._scope = JsonDecoratorScope(UltimakerCloudScope(application)) # type: JsonDecoratorScope + self._cloud_scope = UltimakerCloudScope(application) # type: UltimakerCloudScope + self._json_scope = JsonDecoratorScope(self._cloud_scope) # type: JsonDecoratorScope self._request_urls = {} # type: Dict[str, str] self._to_update = [] # type: List[str] # Package_ids that are waiting to be updated @@ -149,7 +150,7 @@ class Toolbox(QObject, Extension): url = "{base_url}/packages/{package_id}/ratings".format(base_url = CloudApiModel.api_url, package_id = package_id) data = "{\"data\": {\"cura_version\": \"%s\", \"rating\": %i}}" % (Version(self._application.getVersion()), rating) - self._application.getHttpRequestManager().put(url, data = data.encode(), scope = self._scope) + self._application.getHttpRequestManager().put(url, data = data.encode(), scope = self._json_scope) def getLicenseDialogPluginFileLocation(self) -> str: return self._license_dialog_plugin_file_location @@ -539,7 +540,7 @@ class Toolbox(QObject, Extension): self._application.getHttpRequestManager().get(url, callback = callback, error_callback = error_callback, - scope=self._scope) + scope=self._json_scope) @pyqtSlot(str) def startDownload(self, url: str) -> None: @@ -552,7 +553,7 @@ class Toolbox(QObject, Extension): callback = callback, error_callback = error_callback, download_progress_callback = download_progress_callback, - scope=self._scope + scope=self._cloud_scope ) self._download_request_data = request_data From 6db8a9ec3e3734b553e60dfe382b78680a625753 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Fri, 20 Mar 2020 17:31:54 +0100 Subject: [PATCH 21/22] Set progress of backup message to 100% when done CURA-7150 --- plugins/CuraDrive/src/CreateBackupJob.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/CuraDrive/src/CreateBackupJob.py b/plugins/CuraDrive/src/CreateBackupJob.py index f5f557d003..5c3ed529e0 100644 --- a/plugins/CuraDrive/src/CreateBackupJob.py +++ b/plugins/CuraDrive/src/CreateBackupJob.py @@ -65,6 +65,7 @@ class CreateBackupJob(Job): self._job_done.wait() if self.backup_upload_error_message == "": upload_message.setText(catalog.i18nc("@info:backup_status", "Your backup has finished uploading.")) + upload_message.setProgress(upload_message.getMaxProgress()) else: # some error occurred. This error is presented to the user by DrivePluginExtension upload_message.hide() From 1a4cb49ebad4593e1974af937ee80958265d8b24 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Mon, 23 Mar 2020 11:16:33 +0100 Subject: [PATCH 22/22] Hide upload message progress when upload is done CURA-7150 --- plugins/CuraDrive/src/CreateBackupJob.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/CuraDrive/src/CreateBackupJob.py b/plugins/CuraDrive/src/CreateBackupJob.py index 5c3ed529e0..25dc8a4949 100644 --- a/plugins/CuraDrive/src/CreateBackupJob.py +++ b/plugins/CuraDrive/src/CreateBackupJob.py @@ -65,7 +65,7 @@ class CreateBackupJob(Job): self._job_done.wait() if self.backup_upload_error_message == "": upload_message.setText(catalog.i18nc("@info:backup_status", "Your backup has finished uploading.")) - upload_message.setProgress(upload_message.getMaxProgress()) + upload_message.setProgress(None) # Hide progress bar else: # some error occurred. This error is presented to the user by DrivePluginExtension upload_message.hide()