mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-07 23:17:32 -06:00
CuraPackageManager handles packages and plugins
CURA-4644
This commit is contained in:
parent
87c194178d
commit
596e0be7b6
5 changed files with 317 additions and 172 deletions
|
@ -140,7 +140,6 @@ class CuraApplication(QtApplication):
|
||||||
ExtruderStack = Resources.UserType + 9
|
ExtruderStack = Resources.UserType + 9
|
||||||
DefinitionChangesContainer = Resources.UserType + 10
|
DefinitionChangesContainer = Resources.UserType + 10
|
||||||
SettingVisibilityPreset = Resources.UserType + 11
|
SettingVisibilityPreset = Resources.UserType + 11
|
||||||
CuraPackages = Resources.UserType + 12
|
|
||||||
|
|
||||||
Q_ENUMS(ResourceTypes)
|
Q_ENUMS(ResourceTypes)
|
||||||
|
|
||||||
|
@ -190,7 +189,6 @@ class CuraApplication(QtApplication):
|
||||||
Resources.addStorageType(self.ResourceTypes.MachineStack, "machine_instances")
|
Resources.addStorageType(self.ResourceTypes.MachineStack, "machine_instances")
|
||||||
Resources.addStorageType(self.ResourceTypes.DefinitionChangesContainer, "definition_changes")
|
Resources.addStorageType(self.ResourceTypes.DefinitionChangesContainer, "definition_changes")
|
||||||
Resources.addStorageType(self.ResourceTypes.SettingVisibilityPreset, "setting_visibility")
|
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.QualityInstanceContainer, "quality")
|
||||||
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.QualityChangesInstanceContainer, "quality_changes")
|
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.QualityChangesInstanceContainer, "quality_changes")
|
||||||
|
@ -233,7 +231,6 @@ class CuraApplication(QtApplication):
|
||||||
self._simple_mode_settings_manager = None
|
self._simple_mode_settings_manager = None
|
||||||
self._cura_scene_controller = None
|
self._cura_scene_controller = None
|
||||||
self._machine_error_checker = None
|
self._machine_error_checker = None
|
||||||
self._cura_package_manager = None
|
|
||||||
|
|
||||||
self._additional_components = {} # Components to add to certain areas in the interface
|
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",
|
tray_icon_name = "cura-icon-32.png",
|
||||||
**kwargs)
|
**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
|
# FOR TESTING ONLY
|
||||||
if kwargs["parsed_command_line"].get("trigger_early_crash", False):
|
if kwargs["parsed_command_line"].get("trigger_early_crash", False):
|
||||||
assert not "This crash is triggered by the trigger_early_crash command line argument."
|
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()
|
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")
|
Logger.log("i", "Initializing variant manager")
|
||||||
self._variant_manager = VariantManager(container_registry)
|
self._variant_manager = VariantManager(container_registry)
|
||||||
self._variant_manager.initialize()
|
self._variant_manager.initialize()
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from typing import Optional, Dict
|
from typing import Optional
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import urllib.parse
|
|
||||||
import zipfile
|
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.Logger import Logger
|
||||||
from UM.MimeTypeDatabase import MimeTypeDatabase
|
|
||||||
from UM.Settings.InstanceContainer import InstanceContainer
|
|
||||||
from UM.Resources import Resources
|
from UM.Resources import Resources
|
||||||
|
|
||||||
|
from cura.Utils import VersionTools
|
||||||
|
|
||||||
|
|
||||||
class CuraPackageManager(QObject):
|
class CuraPackageManager(QObject):
|
||||||
|
|
||||||
|
@ -27,164 +27,249 @@ class CuraPackageManager(QObject):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
self._application = 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()),
|
self._package_management_file_path = os.path.join(os.path.abspath(Resources.getDataStoragePath()),
|
||||||
"packages.json")
|
"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]+)+$")
|
self._semantic_version_regex = re.compile(r"^[0-9]+(.[0-9]+)+$")
|
||||||
|
|
||||||
|
installedPackagesChanged = pyqtSignal() # Emitted whenever the installed packages collection have been changed.
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
# Load the package management file if exists
|
self._loadManagementData()
|
||||||
if os.path.exists(self._package_management_file_path):
|
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:
|
with open(self._package_management_file_path, "r", encoding = "utf-8") as f:
|
||||||
self._installed_package_dict = json.loads(f.read(), encoding = "utf-8")
|
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)
|
Logger.log("i", "Package management file %s is loaded", self._package_management_file_path)
|
||||||
|
|
||||||
def _saveManagementData(self):
|
def _saveManagementData(self) -> None:
|
||||||
with open(self._package_management_file_path, "w", encoding = "utf-8") as f:
|
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)
|
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)
|
@pyqtSlot(str, result = bool)
|
||||||
def isPackageFile(self, file_name: str):
|
def isPackageFile(self, file_name: str):
|
||||||
|
# TODO: remove this
|
||||||
extension = os.path.splitext(file_name)[1].strip(".")
|
extension = os.path.splitext(file_name)[1].strip(".")
|
||||||
if extension.lower() in ("curapackage",):
|
if extension.lower() in ("curapackage",):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Checks the given package is installed. If so, return a dictionary that contains the package's information.
|
# 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)
|
return self._installed_package_dict.get(package_id)
|
||||||
|
|
||||||
# Gets all installed packages
|
# Checks if the given package is installed.
|
||||||
def getAllInstalledPackages(self) -> Dict[str, dict]:
|
def isPackageInstalled(self, package_id: str) -> bool:
|
||||||
return self._installed_package_dict
|
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)
|
@pyqtSlot(str)
|
||||||
def install(self, file_name: str) -> None:
|
def installPackage(self, filename: str) -> None:
|
||||||
file_url = QUrl(file_name)
|
|
||||||
file_name = file_url.toLocalFile()
|
|
||||||
|
|
||||||
archive = zipfile.ZipFile(file_name)
|
|
||||||
|
|
||||||
# Get package information
|
# Get package information
|
||||||
try:
|
package_info = self.getPackageInfo(filename)
|
||||||
with archive.open("package.json", "r") as f:
|
package_id = package_info["package_id"]
|
||||||
package_info_dict = json.loads(f.read(), encoding = "utf-8")
|
|
||||||
except Exception as e:
|
has_changes = False
|
||||||
raise RuntimeError("Could not get package information from file '%s': %s" % (file_name, e))
|
# 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
|
# Check if it is installed
|
||||||
installed_package = self.getInstalledPackage(package_info_dict["package_id"])
|
installed_package_info = self.getInstalledPackageInfo(package_info["package_id"])
|
||||||
has_installed_version = installed_package is not None
|
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:
|
if to_install_package:
|
||||||
# Remove the existing package first
|
Logger.log("i", "Package [%s] version [%s] is scheduled to be installed.",
|
||||||
Logger.log("i", "Another version of [%s] [%s] has already been installed, will overwrite it with version [%s]",
|
package_id, package_info["package_version"])
|
||||||
installed_package["package_id"], installed_package["package_version"],
|
self._to_install_package_dict[package_id] = {"package_info": package_info,
|
||||||
package_info_dict["package_version"])
|
"filename": filename}
|
||||||
self.remove(package_info_dict["package_id"])
|
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
|
# 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()
|
archive.close()
|
||||||
|
|
||||||
def _installPackage(self, file_name: str, archive: zipfile.ZipFile, package_info_dict: dict):
|
def __installPackageFiles(self, package_id: str, src_dir: str, dst_dir: str, need_to_rename_files: bool = True) -> None:
|
||||||
from cura.CuraApplication import CuraApplication
|
shutil.move(src_dir, dst_dir)
|
||||||
|
|
||||||
package_id = package_info_dict["package_id"]
|
# Rename files if needed
|
||||||
package_type = package_info_dict["package_type"]
|
if not need_to_rename_files:
|
||||||
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)
|
|
||||||
return
|
return
|
||||||
|
for root, _, file_names in os.walk(dst_dir):
|
||||||
package_type = package_info_dict["package_type"]
|
for filename in file_names:
|
||||||
if package_type == "material":
|
new_filename = self.PREFIX_PLACE_HOLDER + package_id + "-" + filename
|
||||||
package_root_dir = Resources.getPath(CuraApplication.ResourceTypes.MaterialInstanceContainer)
|
old_file_path = os.path.join(root, filename)
|
||||||
elif package_type == "quality":
|
new_file_path = os.path.join(root, new_filename)
|
||||||
package_root_dir = Resources.getPath(CuraApplication.ResourceTypes.QualityInstanceContainer)
|
os.rename(old_file_path, new_file_path)
|
||||||
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)
|
|
||||||
|
|
||||||
# Gets package information from the given file.
|
# Gets package information from the given file.
|
||||||
def getPackageInfo(self, file_name: str) -> dict:
|
def getPackageInfo(self, filename: str) -> dict:
|
||||||
archive = zipfile.ZipFile(file_name)
|
archive = zipfile.ZipFile(filename, "r")
|
||||||
try:
|
try:
|
||||||
# All information is in package.json
|
# All information is in package.json
|
||||||
with archive.open("package.json", "r") as f:
|
with archive.open("package.json", "r") as f:
|
||||||
package_info_dict = json.loads(f.read().decode("utf-8"))
|
package_info_dict = json.loads(f.read().decode("utf-8"))
|
||||||
return package_info_dict
|
return package_info_dict
|
||||||
except Exception as e:
|
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:
|
finally:
|
||||||
archive.close()
|
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
|
||||||
|
|
43
cura/Utils/VersionTools.py
Normal file
43
cura/Utils/VersionTools.py
Normal file
|
@ -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
|
0
cura/Utils/__init__.py
Normal file
0
cura/Utils/__init__.py
Normal file
|
@ -1,6 +1,11 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Toolbox is released under the terms of the LGPLv3 or higher.
|
# Toolbox is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import platform
|
||||||
|
|
||||||
from PyQt5.QtCore import QUrl, QObject, pyqtProperty, pyqtSignal, pyqtSlot
|
from PyQt5.QtCore import QUrl, QObject, pyqtProperty, pyqtSignal, pyqtSlot
|
||||||
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
|
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.Qt.Bindings.PluginsModel import PluginsModel
|
||||||
from UM.Extension import Extension
|
from UM.Extension import Extension
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
|
from cura.Utils.VersionTools import compareSemanticVersions
|
||||||
from UM.Version import Version
|
|
||||||
from UM.Message import Message
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
import platform
|
|
||||||
import zipfile
|
|
||||||
|
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
from cura.CuraPackageManager import CuraPackageManager
|
|
||||||
from .AuthorsModel import AuthorsModel
|
from .AuthorsModel import AuthorsModel
|
||||||
from .PackagesModel import PackagesModel
|
from .PackagesModel import PackagesModel
|
||||||
|
|
||||||
i18n_catalog = i18nCatalog("cura")
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
## The Toolbox class is responsible of communicating with the server through the API
|
## The Toolbox class is responsible of communicating with the server through the API
|
||||||
class Toolbox(QObject, Extension):
|
class Toolbox(QObject, Extension):
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self._application = Application.getInstance()
|
||||||
|
self._package_manager = None
|
||||||
self._plugin_registry = Application.getInstance().getPluginRegistry()
|
self._plugin_registry = Application.getInstance().getPluginRegistry()
|
||||||
self._package_manager = None
|
self._package_manager = None
|
||||||
self._packages_version = self._plugin_registry.APIVersion
|
self._packages_version = self._plugin_registry.APIVersion
|
||||||
|
@ -88,6 +87,10 @@ class Toolbox(QObject, Extension):
|
||||||
# installed, or otherwise modified.
|
# installed, or otherwise modified.
|
||||||
self._active_package = None
|
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._dialog = None
|
||||||
self._restartDialog = None
|
self._restartDialog = None
|
||||||
self._restart_required = False
|
self._restart_required = False
|
||||||
|
@ -97,6 +100,7 @@ class Toolbox(QObject, Extension):
|
||||||
# we keep track of the upgraded plugins.
|
# we keep track of the upgraded plugins.
|
||||||
self._newly_installed_plugin_ids = []
|
self._newly_installed_plugin_ids = []
|
||||||
self._newly_uninstalled_plugin_ids = []
|
self._newly_uninstalled_plugin_ids = []
|
||||||
|
self._plugin_statuses = {} # type: Dict[str, str]
|
||||||
|
|
||||||
# variables for the license agreement dialog
|
# variables for the license agreement dialog
|
||||||
self._license_dialog_plugin_name = ""
|
self._license_dialog_plugin_name = ""
|
||||||
|
@ -104,7 +108,11 @@ class Toolbox(QObject, Extension):
|
||||||
self._license_dialog_plugin_file_location = ""
|
self._license_dialog_plugin_file_location = ""
|
||||||
self._restart_dialog_message = ""
|
self._restart_dialog_message = ""
|
||||||
|
|
||||||
# Metadata changes
|
Application.getInstance().initializationFinished.connect(self._onAppInitialized)
|
||||||
|
|
||||||
|
def _onAppInitialized(self):
|
||||||
|
self._package_manager = Application.getInstance().getCuraPackageManager()
|
||||||
|
|
||||||
packagesMetadataChanged = pyqtSignal()
|
packagesMetadataChanged = pyqtSignal()
|
||||||
authorsMetadataChanged = pyqtSignal()
|
authorsMetadataChanged = pyqtSignal()
|
||||||
pluginsShowcaseMetadataChanged = pyqtSignal()
|
pluginsShowcaseMetadataChanged = pyqtSignal()
|
||||||
|
@ -176,29 +184,24 @@ class Toolbox(QObject, Extension):
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def installPlugin(self, file_path):
|
def installPlugin(self, file_path):
|
||||||
# Ensure that it starts with a /, as otherwise it doesn't work on windows.
|
self._package_manager.installPackage(file_path)
|
||||||
if not file_path.startswith("/"):
|
|
||||||
file_path = "/" + file_path
|
|
||||||
result = PluginRegistry.getInstance().installPlugin("file://" + file_path)
|
|
||||||
|
|
||||||
self._newly_installed_plugin_ids.append(result["id"])
|
|
||||||
self.packagesMetadataChanged.emit()
|
self.packagesMetadataChanged.emit()
|
||||||
|
|
||||||
self.openRestartDialog(result["message"])
|
self.openRestartDialog("TODO")
|
||||||
self._restart_required = True
|
self._restart_required = True
|
||||||
self.restartRequiredChanged.emit()
|
self.restartRequiredChanged.emit()
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def removePlugin(self, plugin_id):
|
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.packagesMetadataChanged.emit()
|
||||||
|
|
||||||
self._restart_required = True
|
self._restart_required = True
|
||||||
self.restartRequiredChanged.emit()
|
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)
|
@pyqtSlot(str)
|
||||||
def enablePlugin(self, plugin_id):
|
def enablePlugin(self, plugin_id):
|
||||||
|
@ -261,31 +264,38 @@ class Toolbox(QObject, Extension):
|
||||||
|
|
||||||
# Checks
|
# Checks
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
def _checkCanUpgrade(self, id, version):
|
def _checkCanUpgrade(self, package_id: str, version: str) -> bool:
|
||||||
# Scan plugin server data for plugin with the given id:
|
installed_plugin_data = self._package_manager.getInstalledPackageInfo(package_id)
|
||||||
for plugin in self._packages_metadata:
|
if installed_plugin_data is None:
|
||||||
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
|
return False
|
||||||
|
|
||||||
def _checkInstalled(self, id):
|
installed_version = installed_plugin_data["package_version"]
|
||||||
if id in self._will_uninstall:
|
return compareSemanticVersions(version, installed_version) > 0
|
||||||
return False
|
|
||||||
if id in self._package_manager.getInstalledPackages():
|
def _checkInstalled(self, package_id: str):
|
||||||
return True
|
return self._package_manager.isPackageInstalled(package_id)
|
||||||
if id in self._will_install:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _checkEnabled(self, id):
|
def _checkEnabled(self, id):
|
||||||
if id in self._plugin_registry.getActivePlugins():
|
if id in self._plugin_registry.getActivePlugins():
|
||||||
return True
|
return True
|
||||||
return False
|
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
|
# Make API Calls
|
||||||
|
@ -445,12 +455,19 @@ class Toolbox(QObject, Extension):
|
||||||
def _onDownloadComplete(self, file_path):
|
def _onDownloadComplete(self, file_path):
|
||||||
Logger.log("i", "Toolbox: Download complete.")
|
Logger.log("i", "Toolbox: Download complete.")
|
||||||
print(file_path)
|
print(file_path)
|
||||||
if self._package_manager.isPackageFile(file_path):
|
try:
|
||||||
self._package_manager.install(file_path)
|
package_info = self._package_manager.getPackageInfo(file_path)
|
||||||
|
except:
|
||||||
|
Logger.logException("w", "Toolbox: Package file [%s] was not a valid CuraPackage.", file_path)
|
||||||
return
|
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
|
# Getter & Setters
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue