PluginBrowser.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. # Copyright (c) 2017 Ultimaker B.V.
  2. # PluginBrowser is released under the terms of the AGPLv3 or higher.
  3. from UM.Extension import Extension
  4. from UM.i18n import i18nCatalog
  5. from UM.Logger import Logger
  6. from UM.Qt.ListModel import ListModel
  7. from UM.PluginRegistry import PluginRegistry
  8. from UM.Application import Application
  9. from UM.Version import Version
  10. from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
  11. from PyQt5.QtCore import QUrl, QObject, Qt, pyqtProperty, pyqtSignal, pyqtSlot
  12. from PyQt5.QtQml import QQmlComponent, QQmlContext
  13. import json
  14. import os
  15. import tempfile
  16. i18n_catalog = i18nCatalog("cura")
  17. class PluginBrowser(QObject, Extension):
  18. def __init__(self, parent = None):
  19. super().__init__(parent)
  20. self.addMenuItem(i18n_catalog.i18n("Browse plugins"), self.browsePlugins)
  21. self._api_version = 1
  22. self._api_url = "http://software.ultimaker.com/cura/v%s/" % self._api_version
  23. self._plugin_list_request = None
  24. self._download_plugin_request = None
  25. self._download_plugin_reply = None
  26. self._network_manager = None
  27. self._plugins_metadata = []
  28. self._plugins_model = None
  29. self._qml_component = None
  30. self._qml_context = None
  31. self._dialog = None
  32. self._download_progress = 0
  33. self._is_downloading = False
  34. self._request_header = [b"User-Agent", str.encode("%s - %s" % (Application.getInstance().getApplicationName(), Application.getInstance().getVersion()))]
  35. # Installed plugins are really installed after reboot. In order to prevent the user from downloading the
  36. # same file over and over again, we keep track of the upgraded plugins.
  37. self._newly_installed_plugin_ids = []
  38. pluginsMetadataChanged = pyqtSignal()
  39. onDownloadProgressChanged = pyqtSignal()
  40. onIsDownloadingChanged = pyqtSignal()
  41. @pyqtProperty(bool, notify = onIsDownloadingChanged)
  42. def isDownloading(self):
  43. return self._is_downloading
  44. def browsePlugins(self):
  45. self._createNetworkManager()
  46. self.requestPluginList()
  47. if not self._dialog:
  48. self._createDialog()
  49. self._dialog.show()
  50. @pyqtSlot()
  51. def requestPluginList(self):
  52. Logger.log("i", "Requesting plugin list")
  53. url = QUrl(self._api_url + "plugins")
  54. self._plugin_list_request = QNetworkRequest(url)
  55. self._plugin_list_request.setRawHeader(*self._request_header)
  56. self._network_manager.get(self._plugin_list_request)
  57. def _createDialog(self):
  58. Logger.log("d", "PluginBrowser")
  59. path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "PluginBrowser.qml"))
  60. self._qml_component = QQmlComponent(Application.getInstance()._engine, path)
  61. # We need access to engine (although technically we can't)
  62. self._qml_context = QQmlContext(Application.getInstance()._engine.rootContext())
  63. self._qml_context.setContextProperty("manager", self)
  64. self._dialog = self._qml_component.create(self._qml_context)
  65. if self._dialog is None:
  66. Logger.log("e", "QQmlComponent status %s", self._qml_component.status())
  67. Logger.log("e", "QQmlComponent errorString %s", self._qml_component.errorString())
  68. def setIsDownloading(self, is_downloading):
  69. if self._is_downloading != is_downloading:
  70. self._is_downloading = is_downloading
  71. self.onIsDownloadingChanged.emit()
  72. def _onDownloadPluginProgress(self, bytes_sent, bytes_total):
  73. if bytes_total > 0:
  74. new_progress = bytes_sent / bytes_total * 100
  75. self.setDownloadProgress(new_progress)
  76. if new_progress == 100.0:
  77. self.setIsDownloading(False)
  78. self._download_plugin_reply.downloadProgress.disconnect(self._onDownloadPluginProgress)
  79. # must not delete the temporary file on Windows
  80. self._temp_plugin_file = tempfile.NamedTemporaryFile(mode = "w+b", suffix = ".curaplugin", delete = False)
  81. location = self._temp_plugin_file.name
  82. # write first and close, otherwise on Windows, it cannot read the file
  83. self._temp_plugin_file.write(self._download_plugin_reply.readAll())
  84. self._temp_plugin_file.close()
  85. # open as read
  86. if not location.startswith("/"):
  87. location = "/" + location # Ensure that it starts with a /, as otherwise it doesn't work on windows.
  88. result = PluginRegistry.getInstance().installPlugin("file://" + location)
  89. self._newly_installed_plugin_ids.append(result["id"])
  90. self.pluginsMetadataChanged.emit()
  91. Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Plugin browser"), result["message"])
  92. self._temp_plugin_file.close() # Plugin was installed, delete temp file
  93. @pyqtProperty(int, notify = onDownloadProgressChanged)
  94. def downloadProgress(self):
  95. return self._download_progress
  96. def setDownloadProgress(self, progress):
  97. if progress != self._download_progress:
  98. self._download_progress = progress
  99. self.onDownloadProgressChanged.emit()
  100. @pyqtSlot(str)
  101. def downloadAndInstallPlugin(self, url):
  102. Logger.log("i", "Attempting to download & install plugin from %s", url)
  103. url = QUrl(url)
  104. self._download_plugin_request = QNetworkRequest(url)
  105. self._download_plugin_request.setRawHeader(*self._request_header)
  106. self._download_plugin_reply = self._network_manager.get(self._download_plugin_request)
  107. self.setDownloadProgress(0)
  108. self.setIsDownloading(True)
  109. self._download_plugin_reply.downloadProgress.connect(self._onDownloadPluginProgress)
  110. @pyqtSlot()
  111. def cancelDownload(self):
  112. Logger.log("i", "user cancelled the download of a plugin")
  113. self._download_plugin_reply.abort()
  114. self._download_plugin_reply.downloadProgress.disconnect(self._onDownloadPluginProgress)
  115. self._download_plugin_reply = None
  116. self._download_plugin_request = None
  117. self.setDownloadProgress(0)
  118. self.setIsDownloading(False)
  119. @pyqtProperty(QObject, notify=pluginsMetadataChanged)
  120. def pluginsModel(self):
  121. if self._plugins_model is None:
  122. self._plugins_model = ListModel()
  123. self._plugins_model.addRoleName(Qt.UserRole + 1, "name")
  124. self._plugins_model.addRoleName(Qt.UserRole + 2, "version")
  125. self._plugins_model.addRoleName(Qt.UserRole + 3, "short_description")
  126. self._plugins_model.addRoleName(Qt.UserRole + 4, "author")
  127. self._plugins_model.addRoleName(Qt.UserRole + 5, "already_installed")
  128. self._plugins_model.addRoleName(Qt.UserRole + 6, "file_location")
  129. self._plugins_model.addRoleName(Qt.UserRole + 7, "can_upgrade")
  130. else:
  131. self._plugins_model.clear()
  132. items = []
  133. for metadata in self._plugins_metadata:
  134. items.append({
  135. "name": metadata["label"],
  136. "version": metadata["version"],
  137. "short_description": metadata["short_description"],
  138. "author": metadata["author"],
  139. "already_installed": self._checkAlreadyInstalled(metadata["id"]),
  140. "file_location": metadata["file_location"],
  141. "can_upgrade": self._checkCanUpgrade(metadata["id"], metadata["version"])
  142. })
  143. self._plugins_model.setItems(items)
  144. return self._plugins_model
  145. def _checkCanUpgrade(self, id, version):
  146. plugin_registry = PluginRegistry.getInstance()
  147. metadata = plugin_registry.getMetaData(id)
  148. if metadata != {}:
  149. if id in self._newly_installed_plugin_ids:
  150. return False # We already updated this plugin.
  151. current_version = Version(metadata["plugin"]["version"])
  152. new_version = Version(version)
  153. if new_version > current_version:
  154. return True
  155. return False
  156. def _checkAlreadyInstalled(self, id):
  157. plugin_registry = PluginRegistry.getInstance()
  158. metadata = plugin_registry.getMetaData(id)
  159. if metadata != {}:
  160. return True
  161. else:
  162. if id in self._newly_installed_plugin_ids:
  163. return True # We already installed this plugin, but the registry just doesn't know it yet.
  164. return False
  165. def _onRequestFinished(self, reply):
  166. reply_url = reply.url().toString()
  167. if reply.error() == QNetworkReply.TimeoutError:
  168. Logger.log("w", "Got a timeout.")
  169. # Reset everything.
  170. self.setDownloadProgress(0)
  171. self.setIsDownloading(False)
  172. if self._download_plugin_reply:
  173. self._download_plugin_reply.downloadProgress.disconnect(self._onDownloadPluginProgress)
  174. self._download_plugin_reply.abort()
  175. self._download_plugin_reply = None
  176. return
  177. elif reply.error() == QNetworkReply.HostNotFoundError:
  178. Logger.log("w", "Unable to reach server.")
  179. return
  180. if reply.operation() == QNetworkAccessManager.GetOperation:
  181. if reply_url == self._api_url + "plugins":
  182. try:
  183. json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
  184. self._plugins_metadata = json_data
  185. self.pluginsMetadataChanged.emit()
  186. except json.decoder.JSONDecodeError:
  187. Logger.log("w", "Received an invalid print job state message: Not valid JSON.")
  188. return
  189. else:
  190. # Ignore any operation that is not a get operation
  191. pass
  192. def _onNetworkAccesibleChanged(self, accessible):
  193. if accessible == 0:
  194. self.setDownloadProgress(0)
  195. self.setIsDownloading(False)
  196. if self._download_plugin_reply:
  197. self._download_plugin_reply.downloadProgress.disconnect(self._onDownloadPluginProgress)
  198. self._download_plugin_reply.abort()
  199. self._download_plugin_reply = None
  200. def _createNetworkManager(self):
  201. if self._network_manager:
  202. self._network_manager.finished.disconnect(self._onRequestFinished)
  203. self._network_manager.networkAccessibleChanged.disconnect(self._onNetworkAccesibleChanged)
  204. self._network_manager = QNetworkAccessManager()
  205. self._network_manager.finished.connect(self._onRequestFinished)
  206. self._network_manager.networkAccessibleChanged.connect(self._onNetworkAccesibleChanged)