예제 #1
0
    def _parseResponse(self, reply: "QNetworkReply") -> None:
        """
        Parse the response from the package list API request which can update.

        :param reply: A reply containing information about a number of packages.
        """
        response_data = HttpRequestManager.readJSON(reply)
        if "data" not in response_data:
            Logger.error(
                f"Could not interpret the server's response. Missing 'data' from response data. Keys in response: {response_data.keys()}"
            )
            return
        if len(response_data["data"]) == 0:
            return

        packages = response_data["data"]
        for package in packages:
            self._package_manager.addAvailablePackageVersion(
                package["package_id"], Version(package["package_version"]))
            package_model = self.getPackageModel(package["package_id"])
            if package_model:
                # Also make sure that the local list knows where to get an update
                package_model.setDownloadUrl(package["download_url"])

        self._ongoing_requests["check_updates"] = None
예제 #2
0
    def _parseResponse(self, reply: "QNetworkReply") -> None:
        """
        Parse the response from the package list API request.

        This converts that response into PackageModels, and triggers the ListModel to update.
        :param reply: A reply containing information about a number of packages.
        """
        response_data = HttpRequestManager.readJSON(reply)
        if "data" not in response_data or "links" not in response_data:
            Logger.error(f"Could not interpret the server's response. Missing 'data' or 'links' from response data. Keys in response: {response_data.keys()}")
            self.setErrorMessage(catalog.i18nc("@info:error", "Could not interpret the server's response."))
            return

        for package_data in response_data["data"]:
            try:
                package = PackageModel(package_data, parent = self)
                self._connectManageButtonSignals(package)
                self.appendItem({"package": package})  # Add it to this list model.
            except RuntimeError:
                # Setting the ownership of this object to not qml can still result in a RuntimeError. Which can occur when quickly toggling
                # between de-/constructing RemotePackageLists. This try-except is here to prevent a hard crash when the wrapped C++ object
                # was deleted when it was still parsing the response
                continue

        self._request_url = response_data["links"].get("next", "")  # Use empty string to signify that there is no next page.
        self._ongoing_requests["get_packages"] = None
        self.setIsLoading(False)
        self.setHasMore(self._request_url != "")
예제 #3
0
 def _downloadFinished(self,
                       package_id: str,
                       reply: "QNetworkReply",
                       update: bool = False) -> None:
     with tempfile.NamedTemporaryFile(mode="wb+",
                                      suffix=".curapackage",
                                      delete=False) as temp_file:
         try:
             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)
         except IOError as e:
             Logger.error(
                 f"Failed to write downloaded package to temp file {e}")
             temp_file.close()
             self._downloadError(package_id, update)
         except RuntimeError:
             # Setting the ownership of this object to not qml can still result in a RuntimeError. Which can occur when quickly toggling
             # between de-/constructing Remote or Local PackageLists. This try-except is here to prevent a hard crash when the wrapped C++ object
             # was deleted when it was still parsing the response
             temp_file.close()
             return
     temp_file.close()
     self._to_install[package_id] = temp_file.name
     self._ongoing_requests["download_package"] = None
     self._requestInstall(package_id, update)
예제 #4
0
파일: Toolbox.py 프로젝트: szymongamza/Cura
    def _onDownloadComplete(self, file_path: str) -> None:
        Logger.log("i", "Download complete.")
        package_info = self._package_manager.getPackageInfo(file_path)
        if not package_info:
            Logger.log("w", "Package file [%s] was not a valid CuraPackage.",
                       file_path)
            return

        license_content = self._package_manager.getPackageLicense(file_path)
        package_id = package_info["package_id"]
        if license_content is not None:
            # get the icon url for package_id, make sure the result is a string, never None
            icon_url = next(
                (x["icon_url"]
                 for x in self.packagesModel.items if x["id"] == package_id),
                None) or ""
            self.openLicenseDialog(package_info["display_name"],
                                   license_content, file_path, icon_url)
            return

        installed_id = self.install(file_path)
        if installed_id != package_id:
            Logger.error("Installed package {} does not match {}".format(
                installed_id, package_id))
        self.subscribe(installed_id)
예제 #5
0
    def present(self, plugin_path: str,
                packages: Dict[str, Dict[str, str]]) -> None:
        """Show a license dialog for multiple packages where users can read a license and accept or decline them

        :param plugin_path: Root directory of the Toolbox plugin
        :param packages: Dict[package id, file path]
        """
        if self._presented:
            Logger.error("{clazz} is single-use. Create a new {clazz} instead",
                         clazz=self.__class__.__name__)
            return

        path = os.path.join(plugin_path, self._compatibility_dialog_path)

        self._initState(packages)

        if self._page_count == 0:
            self.licenseAnswers.emit(self._package_models)
            return

        if self._dialog is None:

            context_properties = {
                "catalog": self._catalog,
                "licenseModel": self._license_model,
                "handler": self
            }
            self._dialog = self._app.createQmlComponent(
                path, context_properties)
        self._presentCurrentPackage()
        self._presented = True
예제 #6
0
    def _sendMaterialFile(self, file_path: str, file_name: str,
                          material_id: str) -> None:
        parts = []

        # Add the material file.
        try:
            with open(file_path, "rb") as f:
                parts.append(
                    self.device.createFormPart(
                        "name=\"file\"; filename=\"{file_name}\"".format(
                            file_name=file_name), f.read()))
        except FileNotFoundError:
            Logger.error(
                "Unable to send material {material_id}, since it has been deleted in the meanwhile."
                .format(material_id=material_id))
            return

        # Add the material signature file if needed.
        signature_file_path = "{}.sig".format(file_path)
        if os.path.exists(signature_file_path):
            signature_file_name = os.path.basename(signature_file_path)
            with open(signature_file_path, "rb") as f:
                parts.append(
                    self.device.createFormPart(
                        "name=\"signature_file\"; filename=\"{file_name}\"".
                        format(file_name=signature_file_name), f.read()))

        # FIXME: move form posting to API client
        self.device.postFormWithParts(target="/cluster-api/v1/materials/",
                                      parts=parts,
                                      on_finished=self._sendingFinished)
	def refresh_screenshots(self) -> None:
		"""
		Refresh the screenshots on the current article.

		This starts the process outlined in the ScreenshotTool class.
		"""
		if not has_screenshot_tool:
			Logger.error("The screenshot tool is not installed in this version of the Settings Guide. Please use the version from the source repository.")
			return
		if self.selectedArticleId:  # Refresh a particular article.
			preferences = CuraApplication.getInstance().getPreferences()
			language = preferences.getValue("settings_guide/language")
			if language == "cura_default":
				language = preferences.getValue("general/language")
			refresh_job = threading.Thread(target=ScreenshotTool.refresh_screenshots, kwargs={"article_text": self.articles_source[language][self.selectedArticleId], "refreshed_set": set()})
			refresh_job.start()
		else:  # Refresh everything.
			refreshed_set = set()  # Don't refresh the same image multiple times. Share the same set among all calls.
			def refresh_everything():
				Logger.info("Refreshing ALL screenshots.")
				for article_per_language in self.articles_source.values():
					for article in article_per_language.values():
						ScreenshotTool.refresh_screenshots(article_text=article, refreshed_set=refreshed_set)
			refresh_job = threading.Thread(target=refresh_everything)
			refresh_job.start()
예제 #8
0
    def render(self) -> None:
        if not self._shader:
            try:
                self._shader = OpenGL.getInstance().createShaderProgram(
                    Resources.getPath(Resources.Shaders,
                                      "camera_distance.shader"))
            except InvalidShaderProgramError:
                Logger.error(
                    "Unable to compile shader program: camera_distance.shader")
                return

        width, height = self.getSize()
        self._gl.glViewport(0, 0, width, height)
        self._gl.glClearColor(1.0, 1.0, 1.0, 0.0)
        self._gl.glClear(self._gl.GL_COLOR_BUFFER_BIT
                         | self._gl.GL_DEPTH_BUFFER_BIT)

        # Create a new batch to be rendered
        batch = RenderBatch(self._shader)

        # Fill up the batch with objects that can be sliced. `
        for node in DepthFirstIterator(
                self._scene.getRoot()
        ):  #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
            if node.callDecoration(
                    "isSliceable") and node.getMeshData() and node.isVisible():
                batch.addItem(node.getWorldTransformation(),
                              node.getMeshData())

        self.bind()
        batch.render(self._scene.getActiveCamera())
        self.release()
예제 #9
0
    def adjust_theme(self):
        """
		Makes the tooltips wider, if displaying articles in the tooltips.
		"""
        # Previous versions of the Settings Guide may create a theme, which may become outdated in newer versions.
        # We need to remove it and widen the tooltips in a different way.
        application = CuraApplication.getInstance()
        preferences = application.getPreferences()
        preferences.addPreference(
            "general/theme",
            application.default_theme)  # May not exist yet at this point.
        current_theme = preferences.getValue("general/theme")
        if current_theme and current_theme.endswith("_settingsguideadjust"):
            preferences.setValue("general/theme",
                                 current_theme[:-len("_settingsguideadjust")])
        # Look for Settings Guide adjusted themes in the resources folder.
        theme_path = os.path.dirname(
            Resources.getStoragePath(Resources.Themes, ""))
        try:
            for theme_folder in os.listdir(theme_path):
                if theme_folder.endswith("_settingsguideadjust"):
                    shutil.rmtree(os.path.join(theme_path, theme_folder))
        except EnvironmentError as e:
            # Perhaps no rights? Well, just leave the extra theme in then. Nothing to be done about it.
            Logger.error(
                "Unable to remove Settings Guide deprecated theme: {err}".
                format(err=str(e)))
예제 #10
0
    def _initState(self, packages: Dict[str, Dict[str, Any]]) -> None:

        implicitly_accepted_count = 0

        for package_id, item in packages.items():
            item["package_id"] = package_id
            try:
                item[
                    "licence_content"] = self._package_manager.getPackageLicense(
                        item["package_path"])
            except EnvironmentError as e:
                Logger.error(
                    f"Could not open downloaded package {package_id} to read license file! {type(e)} - {e}"
                )
                continue  # Skip this package.
            if item["licence_content"] is None:
                # Implicitly accept when there is no license
                item["accepted"] = True
                implicitly_accepted_count = implicitly_accepted_count + 1
                self._package_models.append(item)
            else:
                item["accepted"] = None  #: None: no answer yet
                # When presenting the packages, we want to show packages which have a license first.
                # In fact, we don't want to show the others at all because they are implicitly accepted
                self._package_models.insert(0, item)
            CuraApplication.getInstance().processEvents()
        self._page_count = len(
            self._package_models) - implicitly_accepted_count
        self._license_model.setPageCount(self._page_count)
예제 #11
0
    def download(self, model: SubscribedPackagesModel) -> None:
        if self._started:
            Logger.error("Download already started. Create a new %s instead", self.__class__.__name__)
            return

        manager = HttpRequestManager.getInstance()
        for item in model.items:
            package_id = item["package_id"]

            def finishedCallback(reply: QNetworkReply, pid = package_id) -> None:
                self._onFinished(pid, reply)

            def progressCallback(rx: int, rt: int, pid = package_id) -> None:
                self._onProgress(pid, rx, rt)

            def errorCallback(reply: QNetworkReply, error: QNetworkReply.NetworkError, pid = package_id) -> None:
                self._onError(pid)

            request_data = manager.get(
                item["download_url"],
                callback = finishedCallback,
                download_progress_callback = progressCallback,
                error_callback = errorCallback,
                scope = self._scope)

            self._progress[package_id] = {
                "received": 0,
                "total": 1,  # make sure this is not considered done yet. Also divByZero-safe
                "file_written": None,
                "request_data": request_data,
                "package_model": item
            }

        self._started = True
        self._progress_message.show()
예제 #12
0
    def refresh_screenshots(self) -> None:
        """
		Refresh the screenshots on the current article.

		This starts the process outlined in the ScreenshotTool class.
		"""
        if not has_screenshot_tool:
            Logger.error(
                "The screenshot tool is not installed in this version of the Settings Guide. Please use the version from the source repository."
            )
            return
        if self.selectedArticleId:  # Refresh a particular article.
            refresh_job = threading.Thread(
                target=ScreenshotTool.refresh_screenshots,
                kwargs={
                    "article_text": self.selectedArticle,
                    "refreshed_set": set()
                })
            refresh_job.start()
        else:  # Refresh everything.
            refreshed_set = set(
            )  # Don't refresh the same image multiple times. Share the same set among all calls.

            def refresh_everything():
                for article_per_language in self.articles.values():
                    for article in article_per_language.values():
                        ScreenshotTool.refresh_screenshots(
                            article_text=article, refreshed_set=refreshed_set)

            refresh_job = threading.Thread(target=refresh_everything)
            refresh_job.start()
    def present(self, plugin_path: str,
                packages: Dict[str, Dict[str, str]]) -> None:
        if self._presented:
            Logger.error("{clazz} is single-use. Create a new {clazz} instead",
                         clazz=self.__class__.__name__)
            return

        path = os.path.join(plugin_path, self._compatibility_dialog_path)

        self._initState(packages)

        if self._page_count == 0:
            self.licenseAnswers.emit(self._package_models)
            return

        if self._dialog is None:

            context_properties = {
                "catalog": self._catalog,
                "licenseModel": self._license_model,
                "handler": self
            }
            self._dialog = self._app.createQmlComponent(
                path, context_properties)
        self._presentCurrentPackage()
        self._presented = True
예제 #14
0
 def errorCallback(reply: QNetworkReply, error: QNetworkReply.NetworkError, p = project_name,
                   f = file_name) -> None:
     progress_message.hide()
     Logger.error("An error {0} {1} occurred while downloading {2}/{3}".format(str(error), str(reply), p, f))
     Message(
             text = "Failed Digital Library download for '{}'.".format(f),
             title = "Network error {}".format(error),
             lifetime = 10
     ).show()
예제 #15
0
    def _restoreScriptInforFromMetadata(self):
        self.loadAllScripts()
        new_stack = self._global_container_stack
        if new_stack is None:
            return
        self._script_list.clear()
        if not new_stack.getMetaDataEntry(
                "post_processing_scripts"):  # Missing or empty.
            self.scriptListChanged.emit(
            )  # Even emit this if it didn't change. We want it to write the empty list to the stack's metadata.
            self.setSelectedScriptIndex(-1)
            return

        self._script_list.clear()
        scripts_list_strs = new_stack.getMetaDataEntry(
            "post_processing_scripts")
        for script_str in scripts_list_strs.split(
                "\n"
        ):  # Encoded config files should never contain three newlines in a row. At most 2, just before section headers.
            if not script_str:  # There were no scripts in this one (or a corrupt file caused more than 3 consecutive newlines here).
                continue
            script_str = script_str.replace(r"\\\n", "\n").replace(
                r"\\\\", "\\\\")  # Unescape escape sequences.
            script_parser = configparser.ConfigParser(interpolation=None)
            script_parser.optionxform = str  # type: ignore  # Don't transform the setting keys as they are case-sensitive.
            try:
                script_parser.read_string(script_str)
            except configparser.Error as e:
                Logger.error(
                    "Stored post-processing scripts have syntax errors: {err}".
                    format(err=str(e)))
                continue
            for script_name, settings in script_parser.items(
            ):  # There should only be one, really! Otherwise we can't guarantee the order or allow multiple uses of the same script.
                if script_name == "DEFAULT":  # ConfigParser always has a DEFAULT section, but we don't fill it. Ignore this one.
                    continue
                if script_name not in self._loaded_scripts:  # Don't know this post-processing plug-in.
                    Logger.log(
                        "e",
                        "Unknown post-processing script {script_name} was encountered in this global stack."
                        .format(script_name=script_name))
                    continue
                new_script = self._loaded_scripts[script_name]()
                new_script.initialize()
                for setting_key, setting_value in settings.items(
                ):  # Put all setting values into the script.
                    if new_script._instance is not None:
                        new_script._instance.setProperty(
                            setting_key, "value", setting_value)
                self._script_list.append(new_script)

        self.setSelectedScriptIndex(0)
        # Ensure that we always force an update (otherwise the fields don't update correctly!)
        self.selectedIndexChanged.emit()
        self.scriptListChanged.emit()
        self._propertyChanged()
예제 #16
0
 def _getProfileType(self, container_id: str, db_cursor: db.Cursor) -> Optional[str]:
     try:
         db_cursor.execute("select id, container_type from containers where id = ?", (container_id, ))
     except db.DatabaseError as e:
         Logger.error(f"Could not access database: {e}. Is it corrupt? Recreating it.")
         self._recreateCorruptDataBase(db_cursor)
         return None
     row = db_cursor.fetchone()
     if row:
         return row[1]
     return None
예제 #17
0
    def _writeContainerToArchive(container, archive):
        """Helper function that writes ContainerStacks, InstanceContainers and DefinitionContainers to the archive.

        :param container: That follows the :type{ContainerInterface} to archive.
        :param archive: The archive to write to.
        """
        if isinstance(
                container,
                type(ContainerRegistry.getInstance().getEmptyInstanceContainer(
                ))):
            return  # Empty file, do nothing.

        file_suffix = ContainerRegistry.getMimeTypeForContainer(
            type(container)).preferredSuffix

        # Some containers have a base file, which should then be the file to use.
        if "base_file" in container.getMetaData():
            base_file = container.getMetaDataEntry("base_file")
            if base_file != container.getId():
                container = ContainerRegistry.getInstance().findContainers(
                    id=base_file)[0]

        file_name = "Cura/%s.%s" % (container.getId(), file_suffix)

        try:
            if file_name in archive.namelist():
                return  # File was already saved, no need to do it again. Uranium guarantees unique ID's, so this should hold.

            file_in_archive = zipfile.ZipInfo(file_name)
            # For some reason we have to set the compress type of each file as well (it doesn't keep the type of the entire archive)
            file_in_archive.compress_type = zipfile.ZIP_DEFLATED

            # Do not include the network authentication keys
            ignore_keys = {
                "um_cloud_cluster_id",
                "um_network_key",
                "um_linked_to_account",
                "removal_warning",
                "host_guid",
                "group_name",
                "group_size",
                "connection_type",
                "capabilities",
                "octoprint_api_key",
            }
            serialized_data = container.serialize(
                ignored_metadata_keys=ignore_keys)

            archive.writestr(file_in_archive, serialized_data)
        except (FileNotFoundError, EnvironmentError):
            Logger.error(
                "File became inaccessible while writing to it: {archive_filename}"
                .format(archive_filename=archive.fp.name))
            return
예제 #18
0
    def loadPlugins(self, metadata: Optional[Dict[str, Any]] = None) -> None:
        """Load all plugins matching a certain set of metadata

        :param metadata: The meta data that needs to be matched.
        NOTE: This is the method which kicks everything off at app launch.
        """

        start_time = time.time()

        # First load all of the pre-loaded plug-ins.
        for preloaded_plugin in self.preloaded_plugins:
            self.loadPlugin(preloaded_plugin)

        # Get a list of all installed plugins:
        plugin_ids = self._findInstalledPlugins()
        for plugin_id in plugin_ids:
            if plugin_id in self.preloaded_plugins:
                continue  # Already loaded this before.

            self.pluginLoadStarted.emit(plugin_id)

            # Get the plugin metadata:
            try:
                plugin_metadata = self.getMetaData(plugin_id)
            except TrustException:
                Logger.error(
                    "Plugin {} was not loaded because it could not be verified.",
                    plugin_id)
                message_text = i18n_catalog.i18nc(
                    "@error:untrusted",
                    "Plugin {} was not loaded because it could not be verified.",
                    plugin_id)
                Message(text=message_text,
                        message_type=Message.MessageType.ERROR).show()
                continue

            # Save all metadata to the metadata dictionary:
            self._metadata[plugin_id] = plugin_metadata
            if metadata is None or self._subsetInDict(
                    self._metadata[plugin_id], metadata):
                #
                try:
                    self.loadPlugin(plugin_id)
                    QCoreApplication.processEvents(
                    )  # Ensure that the GUI does not freeze.
                    # Add the plugin to the list after actually load the plugin:
                    self._all_plugins.append(plugin_id)
                    self._plugins_installed.append(plugin_id)
                except PluginNotFoundError:
                    pass

        self.pluginLoadStarted.emit("")
        Logger.log("d", "Loading all plugins took %s seconds",
                   time.time() - start_time)
예제 #19
0
 def _downloadError(
         self,
         package_id: str,
         update: bool = False,
         reply: Optional["QNetworkReply"] = None,
         error: Optional["QNetworkReply.NetworkError"] = None) -> None:
     if reply:
         reply_string = bytes(reply.readAll()).decode()
         Logger.error(
             f"Failed to download package: {package_id} due to {reply_string}"
         )
     self._package_manager.packageInstallingFailed.emit(package_id)
예제 #20
0
    def _verifyCleanHierarchy(self, abs_path: str) -> bool:
        """ Only trust plugins if not checking or when there are no other files (not in the exceptions) in parent-root.
        :param abs_path: The path to check for violations.
        :return: True if the hierarchy of the abs_path and below can potentially be trusted.
        """

        # Don't check plugins if not in a 'security scenario'. Hobbyists must be able to tinker without signing.
        if not self._check_if_trusted:
            return True

        install_prefix = os.path.abspath(self._application.getInstallPrefix())

        # Put the parent-root on the work-list:
        worklist = [abs_path]
        while worklist:
            current_dir = worklist.pop()

            # If the directory under scrutiny is a signed folder or bundled, it's ok:
            has_signature_file = os.path.isfile(
                os.path.join(current_dir,
                             TrustBasics.getSignaturesLocalFilename()))
            is_bundled = self._isPathInLocation(install_prefix, current_dir)
            if has_signature_file or is_bundled:
                continue

            # Otherwise it's outside of the trusted area, and needs to be checked whether stray file/folder or plugin:
            for file in os.listdir(current_dir):
                abs_file = os.path.join(current_dir, file)

                # If the file is a sub-folder, put it on the work-list to be investigated in a later iteration:
                if os.path.isdir(abs_file):
                    worklist.append(abs_file)

                # Otherwise, the file can never have a valid signature associated with it, so message and abort:
                else:
                    Logger.error(
                        "Plugins in %s won't load: File that can't be verified: %s",
                        abs_path, abs_file)
                    if abs_path not in self._clean_hierarchy_sent_messages:
                        self._clean_hierarchy_sent_messages.append(abs_path)
                        message_text = i18n_catalog.i18nc(
                            "@error:untrusted",
                            "Plugin {} was not loaded because it tried to load files outside of the trusted context",
                            abs_path)
                        # TODO: Message now has exactly the same string as for an unverified plugin, rather than a
                        #       folder which contains other plugins, because of the string freeze in the current branch.
                        Message(text=message_text).show()
                    return False

        # All is well:
        return True
예제 #21
0
    def onError(self, reply: "QNetworkReply",
                error: Optional["QNetworkReply.NetworkError"]) -> None:
        """
        Used as callback from HTTP requests when the request failed.

        The given network error from the `HttpRequestManager` is logged, and the job is marked as failed.
        :param reply: The main reply of the server. This reply will most likely not be valid.
        :param error: The network error (Qt's enum) that occurred.
        """
        Logger.error(f"Failed to upload material archive: {error}")
        self.failed(
            UploadMaterialsError(
                catalog.i18nc("@text:error",
                              "Failed to connect to Digital Factory.")))
예제 #22
0
    def _createSocket(self, protocol_file: str = None) -> None:
        """Creates a new socket connection."""

        if not protocol_file:
            if not self.getPluginId():
                Logger.error("Can't create socket before CuraEngineBackend plug-in is registered.")
                return
            plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
            if not plugin_path:
                Logger.error("Could not get plugin path!", self.getPluginId())
                return
            protocol_file = os.path.abspath(os.path.join(plugin_path, "Cura.proto"))
        super()._createSocket(protocol_file)
        self._engine_is_fresh = True
예제 #23
0
 def _onError(self, reply: "QNetworkReply", error: Optional[QNetworkReply.NetworkError]) -> None:
     """
     Handles networking and server errors when requesting the list of packages.
     :param reply: The reply with packages. This will most likely be incomplete and should be ignored.
     :param error: The error status of the request.
     """
     if error == QNetworkReply.NetworkError.OperationCanceledError:
         Logger.debug("Cancelled request for packages.")
         self._ongoing_requests["get_packages"] = None
         return  # Don't show an error about this to the user.
     Logger.error("Could not reach Marketplace server.")
     self.setErrorMessage(catalog.i18nc("@info:error", "Could not reach Marketplace."))
     self._ongoing_requests["get_packages"] = None
     self.setIsLoading(False)
예제 #24
0
    def __initializeStoragePaths(cls) -> None:
        Logger.log("d", "Initializing storage paths")
        # use nested structure: <app-name>/<version>/...
        if cls.ApplicationVersion == "master" or cls.ApplicationVersion == "unknown":
            storage_dir_name = os.path.join(cls.ApplicationIdentifier,
                                            cls.ApplicationVersion)
        else:
            version = Version(cls.ApplicationVersion)
            storage_dir_name = os.path.join(
                cls.ApplicationIdentifier,
                "%s.%s" % (version.getMajor(), version.getMinor()))

        # config is saved in "<CONFIG_ROOT>/<storage_dir_name>"
        cls.__config_storage_path = os.path.join(
            Resources._getConfigStorageRootPath(), storage_dir_name)
        Logger.log("d", "Config storage path is %s", cls.__config_storage_path)

        # data is saved in
        #  - on Linux: "<DATA_ROOT>/<storage_dir_name>"
        #  - on other: "<CONFIG_DIR>" (in the config directory)
        data_root_path = Resources._getDataStorageRootPath()
        cls.__data_storage_path = cls.__config_storage_path if data_root_path is None else \
            os.path.join(data_root_path, storage_dir_name)
        Logger.log("d", "Data storage path is %s", cls.__data_storage_path)
        # cache is saved in
        #  - on Linux:   "<CACHE_DIR>/<storage_dir_name>"
        #  - on Windows: "<CACHE_DIR>/<storage_dir_name>/cache"
        #  - on Mac:     "<CONFIG_DIR>/cache" (in the config directory)
        cache_root_path = Resources._getCacheStorageRootPath()
        if cache_root_path is None:
            cls.__cache_storage_path = os.path.join(cls.__config_storage_path,
                                                    "cache")
        else:
            cls.__cache_storage_path = os.path.join(cache_root_path,
                                                    storage_dir_name)
            if Platform.isWindows():
                cls.__cache_storage_path = os.path.join(
                    cls.__cache_storage_path, "cache")
        Logger.log("d", "Cache storage path is %s", cls.__cache_storage_path)
        if not os.path.exists(cls.__config_storage_path) or not os.path.exists(
                cls.__data_storage_path):
            cls._copyLatestDirsIfPresent()
        if not os.path.exists(cls.__cache_storage_path):
            try:
                os.makedirs(cls.__cache_storage_path, exist_ok=True)
            except EnvironmentError as e:
                Logger.error(f"Unable to create cache path: {e}")

        cls.__paths.insert(0, cls.__data_storage_path)
예제 #25
0
    def _removePlugin(self, plugin_id: str) -> None:
        plugin_folder = os.path.join(
            Resources.getStoragePath(Resources.Resources), "plugins")
        plugin_path = os.path.join(plugin_folder, plugin_id)

        if plugin_id in self._bundled_plugin_cache:
            del self.bundled_plugin_cache[plugin_id]

        Logger.log("i", "Attempting to remove plugin '%s' from directory '%s'",
                   plugin_id, plugin_path)
        try:
            shutil.rmtree(plugin_path)
        except EnvironmentError as e:
            Logger.error("Unable to remove plug-in {plugin_id}: {err}".format(
                plugin_id=plugin_id, err=str(e)))
예제 #26
0
    def _onDownloadComplete(self, file_path: str) -> None:
        Logger.log("i", "Download complete.")
        package_info = self._package_manager.getPackageInfo(file_path)
        if not package_info:
            Logger.log("w", "Package file [%s] was not a valid CuraPackage.", file_path)
            return

        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

        package_id = self.install(file_path)
        if package_id != package_info["package_id"]:
            Logger.error("Installed package {} does not match {}".format(package_id, package_info["package_id"]))
        self.subscribe(package_id)
예제 #27
0
    def openSelectedFiles(self) -> None:
        """ Downloads, then opens all files selected in the Qt frontend open dialog.
        """

        temp_dir = tempfile.mkdtemp()
        if temp_dir is None or temp_dir == "":
            Logger.error("Digital Library: Couldn't create temporary directory to store to-be downloaded files.")
            return

        if self._selected_project_idx < 0 or len(self._selected_file_indices) < 1:
            Logger.error("Digital Library: No project or no file selected on open action.")
            return

        to_erase_on_done_set = {
            os.path.join(temp_dir, self._file_model.getItem(i)["fileName"]).replace('\\', '/')
            for i in self._selected_file_indices}

        def onLoadedCallback(filename_done: str) -> None:
            filename_done = os.path.join(temp_dir, filename_done).replace('\\', '/')
            with self._erase_temp_files_lock:
                if filename_done in to_erase_on_done_set:
                    try:
                        os.remove(filename_done)
                        to_erase_on_done_set.remove(filename_done)
                        if len(to_erase_on_done_set) < 1 and os.path.exists(temp_dir):
                            os.rmdir(temp_dir)
                    except (IOError, OSError) as ex:
                        Logger.error("Can't erase temporary (in) {0} because {1}.", temp_dir, str(ex))

            # Save the project id to make sure it will be preselected the next time the user opens the save dialog
            CuraApplication.getInstance().getCurrentWorkspaceInformation().setEntryToStore("digital_factory", "library_project_id", library_project_id)

            # Disconnect the signals so that they are not fired every time another (project) file is loaded
            app.fileLoaded.disconnect(onLoadedCallback)
            app.workspaceLoaded.disconnect(onLoadedCallback)

        app = CuraApplication.getInstance()
        app.fileLoaded.connect(onLoadedCallback)  # fired when non-project files are loaded
        app.workspaceLoaded.connect(onLoadedCallback)  # fired when project files are loaded

        project_name = self._project_model.getItem(self._selected_project_idx)["displayName"]
        for file_index in self._selected_file_indices:
            file_item = self._file_model.getItem(file_index)
            file_name = file_item["fileName"]
            download_url = file_item["downloadUrl"]
            library_project_id = file_item["libraryProjectId"]
            self._openSelectedFile(temp_dir, project_name, file_name, download_url)
예제 #28
0
    def onUploadConfirmed(
            self,
            printer_id: str,
            reply: "QNetworkReply",
            error: Optional["QNetworkReply.NetworkError"] = None) -> None:
        """
        Triggered when we've got a confirmation that the material is synced with the printer, or that syncing failed.

        If syncing succeeded we mark this printer as having the status "success". If it failed we mark the printer as
        "failed". If this is the last upload that needed to be completed, we complete the job with either a success
        state (every printer successfully synced) or a failed state (any printer failed).
        :param printer_id: The printer host_guid that we completed syncing with.
        :param reply: The reply that the server gave to confirm.
        :param error: If the request failed, this error gives an indication what happened.
        """
        if error is not None:
            Logger.error(
                f"Failed to confirm uploading material archive to printer {printer_id}: {error}"
            )
            self._printer_sync_status[
                printer_id] = self.PrinterStatus.FAILED.value
        else:
            self._printer_sync_status[
                printer_id] = self.PrinterStatus.SUCCESS.value

        still_uploading = len([
            val for val in self._printer_sync_status.values()
            if val == self.PrinterStatus.UPLOADING.value
        ])
        self.uploadProgressChanged.emit(
            0.8 + (len(self._printer_sync_status) - still_uploading) /
            len(self._printer_sync_status), self.getPrinterSyncStatus())

        if still_uploading == 0:  # This is the last response to be processed.
            if self.PrinterStatus.FAILED.value in self._printer_sync_status.values(
            ):
                self.setResult(self.Result.FAILED)
                self.setError(
                    UploadMaterialsError(
                        catalog.i18nc(
                            "@text:error",
                            "Failed to connect to Digital Factory to sync materials with some of the printers."
                        )))
            else:
                self.setResult(self.Result.SUCCESS)
            self.uploadCompleted.emit(self.getResult(), self.getError())
예제 #29
0
        def onLoadedCallback(filename_done: str) -> None:
            filename_done = os.path.join(temp_dir, filename_done).replace('\\', '/')
            with self._erase_temp_files_lock:
                if filename_done in to_erase_on_done_set:
                    try:
                        os.remove(filename_done)
                        to_erase_on_done_set.remove(filename_done)
                        if len(to_erase_on_done_set) < 1 and os.path.exists(temp_dir):
                            os.rmdir(temp_dir)
                    except (IOError, OSError) as ex:
                        Logger.error("Can't erase temporary (in) {0} because {1}.", temp_dir, str(ex))

            # Save the project id to make sure it will be preselected the next time the user opens the save dialog
            CuraApplication.getInstance().getCurrentWorkspaceInformation().setEntryToStore("digital_factory", "library_project_id", library_project_id)

            # Disconnect the signals so that they are not fired every time another (project) file is loaded
            app.fileLoaded.disconnect(onLoadedCallback)
            app.workspaceLoaded.disconnect(onLoadedCallback)
예제 #30
0
    def _removePackageModel(self, package_id: str) -> None:
        """
        Cleanup function to remove the package model from the list. Note that this is only done if the package can't
        be updated, it is in the to remove list and isn't in the to be installed list
        """
        package = self.getPackageModel(package_id)

        if package and not package.canUpdate and \
                package_id in self._package_manager.getToRemovePackageIDs() and \
                package_id not in self._package_manager.getPackagesToInstall():
            index = self.find("package", package_id)
            if index < 0:
                Logger.error(
                    f"Could not find card in Listview corresponding with {package_id}"
                )
                self.updatePackages()
                return
            self.removeItem(index)