diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 8ace011209..6e19ab3a30 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -140,7 +140,6 @@ class CuraApplication(QtApplication): ExtruderStack = Resources.UserType + 9 DefinitionChangesContainer = Resources.UserType + 10 SettingVisibilityPreset = Resources.UserType + 11 - CuraPackages = Resources.UserType + 12 Q_ENUMS(ResourceTypes) @@ -190,7 +189,6 @@ class CuraApplication(QtApplication): Resources.addStorageType(self.ResourceTypes.MachineStack, "machine_instances") Resources.addStorageType(self.ResourceTypes.DefinitionChangesContainer, "definition_changes") Resources.addStorageType(self.ResourceTypes.SettingVisibilityPreset, "setting_visibility") - Resources.addStorageType(self.ResourceTypes.CuraPackages, "cura_packages") ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.QualityInstanceContainer, "quality") ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.QualityChangesInstanceContainer, "quality_changes") @@ -233,7 +231,6 @@ class CuraApplication(QtApplication): self._simple_mode_settings_manager = None self._cura_scene_controller = None self._machine_error_checker = None - self._cura_package_manager = None self._additional_components = {} # Components to add to certain areas in the interface @@ -244,6 +241,13 @@ class CuraApplication(QtApplication): tray_icon_name = "cura-icon-32.png", **kwargs) + # Initialize the package manager to remove and install scheduled packages. + from cura.CuraPackageManager import CuraPackageManager + self._cura_package_manager = CuraPackageManager(self) + self._cura_package_manager.initialize() + + self.initialize() + # FOR TESTING ONLY if kwargs["parsed_command_line"].get("trigger_early_crash", False): assert not "This crash is triggered by the trigger_early_crash command line argument." @@ -655,10 +659,6 @@ class CuraApplication(QtApplication): container_registry = ContainerRegistry.getInstance() - from cura.CuraPackageManager import CuraPackageManager - self._cura_package_manager = CuraPackageManager(self) - self._cura_package_manager.initialize() - Logger.log("i", "Initializing variant manager") self._variant_manager = VariantManager(container_registry) self._variant_manager.initialize() diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py index 233e93110b..9c63de751d 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -1,21 +1,21 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Optional, Dict +from typing import Optional import json import os import re import shutil -import urllib.parse import zipfile +import tempfile -from PyQt5.QtCore import pyqtSlot, QObject, QUrl +from PyQt5.QtCore import pyqtSlot, QObject, pyqtSignal from UM.Logger import Logger -from UM.MimeTypeDatabase import MimeTypeDatabase -from UM.Settings.InstanceContainer import InstanceContainer from UM.Resources import Resources +from cura.Utils import VersionTools + class CuraPackageManager(QObject): @@ -27,164 +27,249 @@ class CuraPackageManager(QObject): super().__init__(parent) self._application = parent - self._registry = self._application.getContainerRegistry() + self._container_registry = self._application.getContainerRegistry() + self._plugin_registry = self._application.getPluginRegistry() - # The JSON file that keeps track of all installed packages. + # JSON file that keeps track of all installed packages. self._package_management_file_path = os.path.join(os.path.abspath(Resources.getDataStoragePath()), "packages.json") - self._installed_package_dict = {} # type: Dict[str, dict] + self._installed_package_dict = {} # a dict of all installed packages + self._to_remove_package_set = set() # a set of packages that need to be removed at the next start + self._to_install_package_dict = {} # a dict of packages that need to be installed at the next start self._semantic_version_regex = re.compile(r"^[0-9]+(.[0-9]+)+$") - def initialize(self): - # Load the package management file if exists - if os.path.exists(self._package_management_file_path): - with open(self._package_management_file_path, "r", encoding = "utf-8") as f: - self._installed_package_dict = json.loads(f.read(), encoding = "utf-8") - Logger.log("i", "Package management file %s is loaded", self._package_management_file_path) + installedPackagesChanged = pyqtSignal() # Emitted whenever the installed packages collection have been changed. - def _saveManagementData(self): + def initialize(self): + self._loadManagementData() + self._removeAllScheduledPackages() + self._installAllScheduledPackages() + + # (for initialize) Loads the package management file if exists + def _loadManagementData(self) -> None: + if not os.path.exists(self._package_management_file_path): + Logger.log("i", "Package management file %s doesn't exist, do nothing", self._package_management_file_path) + return + + with open(self._package_management_file_path, "r", encoding = "utf-8") as f: + management_dict = json.loads(f.read(), encoding = "utf-8") + + self._installed_package_dict = management_dict["installed"] + self._to_remove_package_set = set(management_dict["to_remove"]) + self._to_install_package_dict = management_dict["to_install"] + + Logger.log("i", "Package management file %s is loaded", self._package_management_file_path) + + def _saveManagementData(self) -> None: with open(self._package_management_file_path, "w", encoding = "utf-8") as f: - json.dump(self._installed_package_dict, f) + data_dict = {"installed": self._installed_package_dict, + "to_remove": list(self._to_remove_package_set), + "to_install": self._to_install_package_dict} + data_dict["to_remove"] = list(data_dict["to_remove"]) + json.dump(data_dict, f) Logger.log("i", "Package management file %s is saved", self._package_management_file_path) + # (for initialize) Removes all packages that have been scheduled to be removed. + def _removeAllScheduledPackages(self) -> None: + for package_id in self._to_remove_package_set: + self._purgePackage(package_id) + self._to_remove_package_set.clear() + self._saveManagementData() + + # (for initialize) Installs all packages that have been scheduled to be installed. + def _installAllScheduledPackages(self) -> None: + for package_id, installation_package_data in self._to_install_package_dict.items(): + self._installPackage(installation_package_data) + self._to_install_package_dict.clear() + self._saveManagementData() + @pyqtSlot(str, result = bool) def isPackageFile(self, file_name: str): + # TODO: remove this extension = os.path.splitext(file_name)[1].strip(".") if extension.lower() in ("curapackage",): return True return False # Checks the given package is installed. If so, return a dictionary that contains the package's information. - def getInstalledPackage(self, package_id: str) -> Optional[dict]: + def getInstalledPackageInfo(self, package_id: str) -> Optional[dict]: + if package_id in self._to_remove_package_set: + return None + if package_id in self._to_install_package_dict: + return self._to_install_package_dict[package_id]["package_info"] + return self._installed_package_dict.get(package_id) - # Gets all installed packages - def getAllInstalledPackages(self) -> Dict[str, dict]: - return self._installed_package_dict + # Checks if the given package is installed. + def isPackageInstalled(self, package_id: str) -> bool: + return self.getInstalledPackageInfo(package_id) is not None - # Installs the given package file. + # Schedules the given package file to be installed upon the next start. @pyqtSlot(str) - def install(self, file_name: str) -> None: - file_url = QUrl(file_name) - file_name = file_url.toLocalFile() - - archive = zipfile.ZipFile(file_name) - + def installPackage(self, filename: str) -> None: # Get package information - try: - with archive.open("package.json", "r") as f: - package_info_dict = json.loads(f.read(), encoding = "utf-8") - except Exception as e: - raise RuntimeError("Could not get package information from file '%s': %s" % (file_name, e)) + package_info = self.getPackageInfo(filename) + package_id = package_info["package_id"] + + has_changes = False + # Check the delayed installation and removal lists first + if package_id in self._to_remove_package_set: + self._to_remove_package_set.remove(package_id) + has_changes = True # Check if it is installed - installed_package = self.getInstalledPackage(package_info_dict["package_id"]) - has_installed_version = installed_package is not None + installed_package_info = self.getInstalledPackageInfo(package_info["package_id"]) + to_install_package = installed_package_info is None # Install if the package has not been installed + if installed_package_info is not None: + # Compare versions and only schedule the installation if the given package is newer + new_version = package_info["package_version"] + installed_version = installed_package_info["package_version"] + if VersionTools.compareSemanticVersions(new_version, installed_version) > 0: + Logger.log("i", "Package [%s] version [%s] is newer than the installed version [%s], update it.", + package_id, new_version, installed_version) + to_install_package = True - if has_installed_version: - # Remove the existing package first - Logger.log("i", "Another version of [%s] [%s] has already been installed, will overwrite it with version [%s]", - installed_package["package_id"], installed_package["package_version"], - package_info_dict["package_version"]) - self.remove(package_info_dict["package_id"]) + if to_install_package: + Logger.log("i", "Package [%s] version [%s] is scheduled to be installed.", + package_id, package_info["package_version"]) + self._to_install_package_dict[package_id] = {"package_info": package_info, + "filename": filename} + has_changes = True + + self._saveManagementData() + if has_changes: + self.installedPackagesChanged.emit() + + # Schedules the given package to be removed upon the next start. + @pyqtSlot(str) + def removePackage(self, package_id: str) -> None: + # Check the delayed installation and removal lists first + if not self.isPackageInstalled(package_id): + Logger.log("i", "Attempt to remove package [%s] that is not installed, do nothing.", package_id) + return + + # Remove from the delayed installation list if present + if package_id in self._to_install_package_dict: + del self._to_install_package_dict[package_id] + + # If the package has already been installed, schedule for a delayed removal + if package_id in self._installed_package_dict: + self._to_remove_package_set.add(package_id) + + self._saveManagementData() + self.installedPackagesChanged.emit() + + # Removes everything associated with the given package ID. + def _purgePackage(self, package_id: str) -> None: + # Get all folders that need to be checked for installed packages, including: + # - materials + # - qualities + # - plugins + from cura.CuraApplication import CuraApplication + dirs_to_check = [ + Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer), + Resources.getStoragePath(CuraApplication.ResourceTypes.QualityInstanceContainer), + os.path.join(os.path.abspath(Resources.getDataStoragePath()), "plugins"), + ] + + for root_dir in dirs_to_check: + package_dir = os.path.join(root_dir, package_id) + if os.path.exists(package_dir): + Logger.log("i", "Removing '%s' for package [%s]", package_dir, package_id) + shutil.rmtree(package_dir) + + # Installs all files associated with the given package. + def _installPackage(self, installation_package_data: dict): + package_info = installation_package_data["package_info"] + filename = installation_package_data["filename"] + + package_id = package_info["package_id"] + + Logger.log("i", "Installing package [%s] from file [%s]", package_id, filename) + + # If it's installed, remove it first and then install + if package_id in self._installed_package_dict: + self._purgePackage(package_id) # Install the package - self._installPackage(file_name, archive, package_info_dict) + archive = zipfile.ZipFile(filename, "r") + + temp_dir = tempfile.TemporaryDirectory() + archive.extractall(temp_dir.name) + + from cura.CuraApplication import CuraApplication + installation_dirs_dict = { + "materials": Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer), + "quality": Resources.getStoragePath(CuraApplication.ResourceTypes.QualityInstanceContainer), + "plugins": os.path.join(os.path.abspath(Resources.getDataStoragePath()), "plugins"), + } + + for sub_dir_name, installation_root_dir in installation_dirs_dict.items(): + src_dir_path = os.path.join(temp_dir.name, "files", sub_dir_name) + dst_dir_path = os.path.join(installation_root_dir, package_id) + + if not os.path.exists(src_dir_path): + continue + + # Need to rename the container files so they don't get ID conflicts + to_rename_files = sub_dir_name not in ("plugins",) + self.__installPackageFiles(package_id, src_dir_path, dst_dir_path, need_to_rename_files= to_rename_files) archive.close() - def _installPackage(self, file_name: str, archive: zipfile.ZipFile, package_info_dict: dict): - from cura.CuraApplication import CuraApplication + def __installPackageFiles(self, package_id: str, src_dir: str, dst_dir: str, need_to_rename_files: bool = True) -> None: + shutil.move(src_dir, dst_dir) - package_id = package_info_dict["package_id"] - package_type = package_info_dict["package_type"] - if package_type == "material": - package_root_dir = Resources.getPath(CuraApplication.ResourceTypes.MaterialInstanceContainer) - material_class = self._registry.getContainerForMimeType(MimeTypeDatabase.getMimeType("application/x-ultimaker-material-profile")) - file_extension = self._registry.getMimeTypeForContainer(material_class).preferredSuffix - elif package_type == "quality": - package_root_dir = Resources.getPath(CuraApplication.ResourceTypes.QualityInstanceContainer) - file_extension = "." + self._registry.getMimeTypeForContainer(InstanceContainer).preferredSuffix - else: - raise RuntimeError("Unexpected package type [%s] in file [%s]" % (package_type, file_name)) - - Logger.log("i", "Prepare package directory [%s]", package_root_dir) - - # get package directory - package_installation_path = os.path.join(os.path.abspath(package_root_dir), package_id) - if os.path.exists(package_installation_path): - Logger.log("w", "Path [%s] exists, removing it.", package_installation_path) - if os.path.isfile(package_installation_path): - os.remove(package_installation_path) - else: - shutil.rmtree(package_installation_path, ignore_errors = True) - - os.makedirs(package_installation_path, exist_ok = True) - - # Only extract the needed files - for file_info in archive.infolist(): - if file_info.is_dir(): - continue - - file_name = os.path.basename(file_info.filename) - if not file_name.endswith(file_extension): - continue - - # Generate new file name and save to file - new_file_name = urllib.parse.quote_plus(self.PREFIX_PLACE_HOLDER + package_id + "-" + file_name) - new_file_path = os.path.join(package_installation_path, new_file_name) - with archive.open(file_info.filename, "r") as f: - content = f.read() - with open(new_file_path, "wb") as f2: - f2.write(content) - - Logger.log("i", "Installed package file to [%s]", new_file_name) - - self._installed_package_dict[package_id] = package_info_dict - self._saveManagementData() - Logger.log("i", "Package [%s] has been installed", package_id) - - # Removes a package with the given package ID. - @pyqtSlot(str) - def remove(self, package_id: str) -> None: - from cura.CuraApplication import CuraApplication - - package_info_dict = self.getInstalledPackage(package_id) - if package_info_dict is None: - Logger.log("w", "Attempt to remove non-existing package [%s], do nothing.", package_id) + # Rename files if needed + if not need_to_rename_files: return - - package_type = package_info_dict["package_type"] - if package_type == "material": - package_root_dir = Resources.getPath(CuraApplication.ResourceTypes.MaterialInstanceContainer) - elif package_type == "quality": - package_root_dir = Resources.getPath(CuraApplication.ResourceTypes.QualityInstanceContainer) - else: - raise RuntimeError("Unexpected package type [%s] for package [%s]" % (package_type, package_id)) - - # Get package directory - package_installation_path = os.path.join(os.path.abspath(package_root_dir), package_id) - if os.path.exists(package_installation_path): - if os.path.isfile(package_installation_path): - os.remove(package_installation_path) - else: - shutil.rmtree(package_installation_path, ignore_errors=True) - - if package_id in self._installed_package_dict: - del self._installed_package_dict[package_id] - self._saveManagementData() - Logger.log("i", "Package [%s] has been removed.", package_id) + for root, _, file_names in os.walk(dst_dir): + for filename in file_names: + new_filename = self.PREFIX_PLACE_HOLDER + package_id + "-" + filename + old_file_path = os.path.join(root, filename) + new_file_path = os.path.join(root, new_filename) + os.rename(old_file_path, new_file_path) # Gets package information from the given file. - def getPackageInfo(self, file_name: str) -> dict: - archive = zipfile.ZipFile(file_name) + def getPackageInfo(self, filename: str) -> dict: + archive = zipfile.ZipFile(filename, "r") try: # All information is in package.json with archive.open("package.json", "r") as f: package_info_dict = json.loads(f.read().decode("utf-8")) return package_info_dict except Exception as e: - raise RuntimeError("Could not get package information from file '%s': %s" % (e, file_name)) + raise RuntimeError("Could not get package information from file '%s': %s" % (filename, e)) finally: archive.close() + + # Gets the license file content if present in the given package file. + # Returns None if there is no license file found. + def getPackageLicense(self, filename: str) -> Optional[str]: + license_string = None + archive = zipfile.ZipFile(filename) + try: + # Go through all the files and use the first successful read as the result + for file_info in archive.infolist(): + if file_info.is_dir() or not file_info.filename.startswith("files/"): + continue + + filename_parts = os.path.basename(file_info.filename.lower()).split(".") + stripped_filename = filename_parts[0] + if stripped_filename in ("license", "licence"): + Logger.log("i", "Found potential license file '%s'", file_info.filename) + try: + with archive.open(file_info.filename, "r") as f: + data = f.read() + license_string = data.decode("utf-8") + break + except: + Logger.logException("e", "Failed to load potential license file '%s' as text file.", + file_info.filename) + license_string = None + except Exception as e: + raise RuntimeError("Could not get package license from file '%s': %s" % (filename, e)) + finally: + archive.close() + return license_string diff --git a/cura/Utils/VersionTools.py b/cura/Utils/VersionTools.py new file mode 100644 index 0000000000..ab0ac6d813 --- /dev/null +++ b/cura/Utils/VersionTools.py @@ -0,0 +1,43 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +import re + + +# Regex for checking if a string is a semantic version number +_SEMANTIC_VERSION_REGEX = re.compile(r"^[0-9]+(.[0-9]+)+$") + + +# Checks if the given version string is a valid semantic version number. +def isSemanticVersion(version: str) -> bool: + return _SEMANTIC_VERSION_REGEX.match(version) is not None + + +# Compares the two given semantic version strings and returns: +# -1 if version1 < version2 +# 0 if version1 == version2 +# +1 if version1 > version2 +# Note that this function only works with semantic versions such as "a.b.c..." +def compareSemanticVersions(version1: str, version2: str) -> int: + # Validate the version numbers first + for version in (version1, version2): + if not isSemanticVersion(version): + raise ValueError("Invalid Package version '%s'" % version) + + # Split the version strings into lists of integers + version1_parts = [int(p) for p in version1.split(".")] + version2_parts = [int(p) for p in version2.split(".")] + max_part_length = max(len(version1_parts), len(version2_parts)) + + # Make sure that two versions have the same number of parts. For missing parts, just append 0s. + for parts in (version1_parts, version2_parts): + for _ in range(max_part_length - len(parts)): + parts.append(0) + + # Compare the version parts and return the result + result = 0 + for idx in range(max_part_length): + result = version1_parts[idx] - version2_parts[idx] + if result != 0: + break + return result diff --git a/cura/Utils/__init__.py b/cura/Utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index bf702c34b7..d88a1c0f37 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -1,6 +1,11 @@ # Copyright (c) 2018 Ultimaker B.V. # Toolbox is released under the terms of the LGPLv3 or higher. + from typing import Dict +import json +import os +import tempfile +import platform from PyQt5.QtCore import QUrl, QObject, pyqtProperty, pyqtSignal, pyqtSlot from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply @@ -11,28 +16,22 @@ from UM.PluginRegistry import PluginRegistry from UM.Qt.Bindings.PluginsModel import PluginsModel from UM.Extension import Extension from UM.i18n import i18nCatalog - -from UM.Version import Version -from UM.Message import Message - -import json -import os -import tempfile -import platform -import zipfile +from cura.Utils.VersionTools import compareSemanticVersions from cura.CuraApplication import CuraApplication -from cura.CuraPackageManager import CuraPackageManager from .AuthorsModel import AuthorsModel from .PackagesModel import PackagesModel i18n_catalog = i18nCatalog("cura") + ## The Toolbox class is responsible of communicating with the server through the API class Toolbox(QObject, Extension): def __init__(self, parent=None): super().__init__(parent) + self._application = Application.getInstance() + self._package_manager = None self._plugin_registry = Application.getInstance().getPluginRegistry() self._package_manager = None self._packages_version = self._plugin_registry.APIVersion @@ -88,6 +87,10 @@ class Toolbox(QObject, Extension): # installed, or otherwise modified. self._active_package = None + # Nowadays can be 'plugins', 'materials' or 'installed' + self._current_view = "plugins" + self._detail_data = {} # Extraneous since can just use the data prop of the model. + self._dialog = None self._restartDialog = None self._restart_required = False @@ -97,6 +100,7 @@ class Toolbox(QObject, Extension): # we keep track of the upgraded plugins. self._newly_installed_plugin_ids = [] self._newly_uninstalled_plugin_ids = [] + self._plugin_statuses = {} # type: Dict[str, str] # variables for the license agreement dialog self._license_dialog_plugin_name = "" @@ -104,7 +108,11 @@ class Toolbox(QObject, Extension): self._license_dialog_plugin_file_location = "" self._restart_dialog_message = "" - # Metadata changes + Application.getInstance().initializationFinished.connect(self._onAppInitialized) + + def _onAppInitialized(self): + self._package_manager = Application.getInstance().getCuraPackageManager() + packagesMetadataChanged = pyqtSignal() authorsMetadataChanged = pyqtSignal() pluginsShowcaseMetadataChanged = pyqtSignal() @@ -176,29 +184,24 @@ class Toolbox(QObject, Extension): @pyqtSlot(str) def installPlugin(self, file_path): - # Ensure that it starts with a /, as otherwise it doesn't work on windows. - if not file_path.startswith("/"): - file_path = "/" + file_path - result = PluginRegistry.getInstance().installPlugin("file://" + file_path) + self._package_manager.installPackage(file_path) - self._newly_installed_plugin_ids.append(result["id"]) self.packagesMetadataChanged.emit() - self.openRestartDialog(result["message"]) + self.openRestartDialog("TODO") self._restart_required = True self.restartRequiredChanged.emit() @pyqtSlot(str) def removePlugin(self, plugin_id): - result = PluginRegistry.getInstance().uninstallPlugin(plugin_id) + self._package_manager.removePackage(plugin_id) - self._newly_uninstalled_plugin_ids.append(result["id"]) self.packagesMetadataChanged.emit() self._restart_required = True self.restartRequiredChanged.emit() - Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Plugin browser"), result["message"]) + Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Plugin browser"), "TODO") @pyqtSlot(str) def enablePlugin(self, plugin_id): @@ -261,31 +264,38 @@ class Toolbox(QObject, Extension): # Checks # -------------------------------------------------------------------------- - def _checkCanUpgrade(self, id, version): - # Scan plugin server data for plugin with the given id: - for plugin in self._packages_metadata: - if id == plugin["id"]: - reg_version = Version(version) - new_version = Version(plugin["version"]) - if new_version > reg_version: - Logger.log("i", "%s has an update availible: %s", plugin["id"], plugin["version"]) - return True - return False - - def _checkInstalled(self, id): - if id in self._will_uninstall: + def _checkCanUpgrade(self, package_id: str, version: str) -> bool: + installed_plugin_data = self._package_manager.getInstalledPackageInfo(package_id) + if installed_plugin_data is None: return False - if id in self._package_manager.getInstalledPackages(): - return True - if id in self._will_install: - return True - return False + + installed_version = installed_plugin_data["package_version"] + return compareSemanticVersions(version, installed_version) > 0 + + def _checkInstalled(self, package_id: str): + return self._package_manager.isPackageInstalled(package_id) def _checkEnabled(self, id): if id in self._plugin_registry.getActivePlugins(): return True return False + def _createNetworkManager(self): + if self._network_manager: + self._network_manager.finished.disconnect(self._onRequestFinished) + self._network_manager.networkAccessibleChanged.disconnect(self._onNetworkAccesibleChanged) + self._network_manager = QNetworkAccessManager() + self._network_manager.finished.connect(self._onRequestFinished) + self._network_manager.networkAccessibleChanged.connect(self._onNetworkAccesibleChanged) + + @pyqtProperty(bool, notify = restartRequiredChanged) + def restartRequired(self): + return self._restart_required + + @pyqtSlot() + def restart(self): + CuraApplication.getInstance().windowClosed() + # Make API Calls @@ -445,12 +455,19 @@ class Toolbox(QObject, Extension): def _onDownloadComplete(self, file_path): Logger.log("i", "Toolbox: Download complete.") print(file_path) - if self._package_manager.isPackageFile(file_path): - self._package_manager.install(file_path) + try: + package_info = self._package_manager.getPackageInfo(file_path) + except: + Logger.logException("w", "Toolbox: Package file [%s] was not a valid CuraPackage.", file_path) return - else: - Logger.log("w", "Toolbox: Package was not a valid CuraPackage.") + license_content = self._package_manager.getPackageLicense(file_path) + if license_content is not None: + self.openLicenseDialog(package_info["package_id"], license_content, file_path) + return + + self._package_manager.installPlugin(file_path) + return # Getter & Setters