From 3b3d9860581860939b4ef257e44045ef15c2ad27 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 2 Dec 2021 18:02:49 +0100 Subject: [PATCH] Groundwork for installing/updating packages Contributes to: CURA-8587 --- cura/CuraPackageManager.py | 6 +- plugins/Marketplace/LocalPackageList.py | 5 +- plugins/Marketplace/PackageList.py | 68 +++++++++++++++++-- .../resources/qml/ManageButton.qml | 8 +-- 4 files changed, 73 insertions(+), 14 deletions(-) diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py index a8400bfae7..34d8c5c61f 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -20,12 +20,16 @@ class CuraPackageManager(PackageManager): def __init__(self, application: "QtApplication", parent: Optional["QObject"] = None) -> None: super().__init__(application, parent) self._locally_installed_packages = None + self.installedPackagesChanged.connect(self._updateLocallyInstalledPackages) + + def _updateLocallyInstalledPackages(self): + self._locally_installed_packages = list(self.iterateAllLocalPackages()) @property def locally_installed_packages(self): """locally installed packages, lazy execution""" if self._locally_installed_packages is None: - self._locally_installed_packages = list(self.iterateAllLocalPackages()) + self._updateLocallyInstalledPackages() return self._locally_installed_packages @locally_installed_packages.setter diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py index be805fb002..4721224e16 100644 --- a/plugins/Marketplace/LocalPackageList.py +++ b/plugins/Marketplace/LocalPackageList.py @@ -89,7 +89,8 @@ class LocalPackageList(PackageList): return for package_data in response_data["data"]: - index = self.find("package", package_data["package_id"]) - self.getItem(index)["package"].canUpdate = True + package = self._getPackageModel(package_data["package_id"]) + package.download_url = package_data.get("download_url", "") + package.canUpdate = True self.sort(attrgetter("sectionTitle", "canUpdate", "displayName"), key = "package", reverse = True) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 1ce8d3fe1d..32eb302e97 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -1,18 +1,21 @@ # Copyright (c) 2021 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +import tempfile from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, Qt -from typing import Optional, TYPE_CHECKING +from typing import Dict, Optional, TYPE_CHECKING from UM.i18n import i18nCatalog from UM.Qt.ListModel import ListModel -from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope # To request JSON responses from the API. -from UM.TaskManagement.HttpRequestManager import HttpRequestData # To request the package list from the API +from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope +from UM.TaskManagement.HttpRequestManager import HttpRequestData , HttpRequestManager from UM.Logger import Logger from cura.CuraApplication import CuraApplication from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope # To make requests to the Ultimaker API with correct authorization. +from .PackageModel import PackageModel + if TYPE_CHECKING: from PyQt5.QtCore import QObject @@ -24,6 +27,7 @@ class PackageList(ListModel): such as Packages obtained from Remote or Local source """ PackageRole = Qt.UserRole + 1 + DISK_WRITE_BUFFER_SIZE = 256 * 1024 # 256 KB def __init__(self, parent: Optional["QObject"] = None) -> None: super().__init__(parent) @@ -33,6 +37,8 @@ class PackageList(ListModel): self._is_loading = False self._has_more = False self._has_footer = True + self._to_install: Dict[str, str] = {} + self.canInstallChanged.connect(self._install) self._ongoing_request: Optional[HttpRequestData] = None self._scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) @@ -105,13 +111,60 @@ class PackageList(ListModel): def _connectManageButtonSignals(self, package): package.installPackageTriggered.connect(self.installPackage) package.uninstallPackageTriggered.connect(self.uninstallPackage) - package.updatePackageTriggered.connect(self.updatePackage) + package.updatePackageTriggered.connect(self.installPackage) package.enablePackageTriggered.connect(self.enablePackage) package.disablePackageTriggered.connect(self.disablePackage) + def _getPackageModel(self, package_id: str) -> PackageModel: + index = self.find("package", package_id) + return self.getItem(index)["package"] + + canInstallChanged = pyqtSignal(str, bool) + + def download(self, package_id, url, update: bool = False): + + def downloadFinished(reply: "QNetworkReply") -> None: + self._downloadFinished(package_id, reply, update) + + HttpRequestManager.getInstance().get( + url, + scope = self._scope, + callback = downloadFinished + ) + + def _downloadFinished(self, package_id: str, reply: "QNetworkReply", update: bool = False) -> None: + try: + with tempfile.NamedTemporaryFile(mode = "wb+", suffix = ".curapackage", delete = False) as temp_file: + bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) + while bytes_read: + temp_file.write(bytes_read) + bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) + Logger.debug(f"Finished downloading {package_id} and stored it as {temp_file.name}") + self._to_install[package_id] = temp_file.name + self.canInstallChanged.emit(package_id, update) + except IOError as e: + Logger.logException("e", "Failed to write downloaded package to temp file", e) + temp_file.close() + @pyqtSlot(str) - def installPackage(self, package_id): + def installPackage(self, package_id: str) -> None: + package = self._getPackageModel(package_id) + url = package.download_url + Logger.debug(f"Trying to download and install {package_id} from {url}") + self.download(package_id, url) + + def _install(self, package_id: str, update: bool = False) -> None: + package_path = self._to_install.pop(package_id) Logger.debug(f"Installing {package_id}") + to_be_installed = self._manager.installPackage(package_path) != None + package = self._getPackageModel(package_id) + if package.canUpdate and to_be_installed: + package.canUpdate = False + package.setManageInstallState(to_be_installed) + if update: + package.setIsUpdating(False) + else: + package.setIsInstalling(False) @pyqtSlot(str) def uninstallPackage(self, package_id): @@ -119,7 +172,10 @@ class PackageList(ListModel): @pyqtSlot(str) def updatePackage(self, package_id): - Logger.debug(f"Updating {package_id}") + package = self._getPackageModel(package_id) + url = package.download_url + Logger.debug(f"Trying to download and update {package_id} from {url}") + self.download(package_id, url, True) @pyqtSlot(str) def enablePackage(self, package_id): diff --git a/plugins/Marketplace/resources/qml/ManageButton.qml b/plugins/Marketplace/resources/qml/ManageButton.qml index 035c369fd8..797e83830f 100644 --- a/plugins/Marketplace/resources/qml/ManageButton.qml +++ b/plugins/Marketplace/resources/qml/ManageButton.qml @@ -17,11 +17,11 @@ RowLayout property string busySecondaryText: busyMessageText.text property string mainState: "primary" property bool enabled: true - readonly property bool busy: state == "busy" + property bool busy: false signal clicked(bool primary_action) - state: mainState + state: busy ? "busy" : mainState Cura.PrimaryButton { @@ -32,7 +32,6 @@ RowLayout onClicked: { manageButton.clicked(true) - manageButton.state = "busy" } } @@ -45,7 +44,6 @@ RowLayout onClicked: { manageButton.clicked(false) - manageButton.state = "busy" } } @@ -155,7 +153,7 @@ RowLayout PropertyChanges { target: busyMessage - visible: true + visible: manageButton.visible } } ]