DownloadPresenter.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. # Copyright (c) 2022 UltiMaker
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import tempfile
  4. from typing import Dict, List, Any
  5. from PyQt6.QtNetwork import QNetworkReply
  6. from UM.i18n import i18nCatalog
  7. from UM.Logger import Logger
  8. from UM.Message import Message
  9. from UM.Signal import Signal
  10. from UM.TaskManagement.HttpRequestManager import HttpRequestManager
  11. from cura.CuraApplication import CuraApplication
  12. from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
  13. from .SubscribedPackagesModel import SubscribedPackagesModel
  14. i18n_catalog = i18nCatalog("cura")
  15. class DownloadPresenter:
  16. """Downloads a set of packages from the Ultimaker Cloud Marketplace
  17. use download() exactly once: should not be used for multiple sets of downloads since this class contains state
  18. """
  19. DISK_WRITE_BUFFER_SIZE = 256 * 1024 # 256 KB
  20. def __init__(self, app: CuraApplication) -> None:
  21. # Emits (Dict[str, str], List[str]) # (success_items, error_items)
  22. # Dict{success_package_id, temp_file_path}
  23. # List[errored_package_id]
  24. self.done = Signal()
  25. self._app = app
  26. self._scope = UltimakerCloudScope(app)
  27. self._started = False
  28. self._progress_message = self._createProgressMessage()
  29. self._progress: Dict[str, Dict[str, Any]] = {}
  30. self._error: List[str] = []
  31. def download(self, model: SubscribedPackagesModel) -> None:
  32. if self._started:
  33. Logger.error("Download already started. Create a new %s instead", self.__class__.__name__)
  34. return
  35. manager = HttpRequestManager.getInstance()
  36. for item in model.items:
  37. package_id = item["package_id"]
  38. def finishedCallback(reply: QNetworkReply, pid = package_id) -> None:
  39. self._onFinished(pid, reply)
  40. def progressCallback(rx: int, rt: int, pid = package_id) -> None:
  41. self._onProgress(pid, rx, rt)
  42. def errorCallback(reply: QNetworkReply, error: QNetworkReply.NetworkError, pid = package_id) -> None:
  43. self._onError(pid)
  44. request_data = manager.get(
  45. item["download_url"],
  46. callback = finishedCallback,
  47. download_progress_callback = progressCallback,
  48. error_callback = errorCallback,
  49. scope = self._scope)
  50. self._progress[package_id] = {
  51. "received": 0,
  52. "total": 1, # make sure this is not considered done yet. Also divByZero-safe
  53. "file_written": None,
  54. "request_data": request_data,
  55. "package_model": item
  56. }
  57. self._started = True
  58. self._progress_message.show()
  59. def abort(self) -> None:
  60. manager = HttpRequestManager.getInstance()
  61. for item in self._progress.values():
  62. manager.abortRequest(item["request_data"])
  63. # Aborts all current operations and returns a copy with the same settings such as app and scope
  64. def resetCopy(self) -> "DownloadPresenter":
  65. self.abort()
  66. self.done.disconnectAll()
  67. return DownloadPresenter(self._app)
  68. def _createProgressMessage(self) -> Message:
  69. return Message(i18n_catalog.i18nc("@info:generic", "Syncing..."),
  70. lifetime = 0,
  71. use_inactivity_timer = False,
  72. progress = 0.0,
  73. title = i18n_catalog.i18nc("@info:title", "Changes detected from your UltiMaker account"))
  74. def _onFinished(self, package_id: str, reply: QNetworkReply) -> None:
  75. self._progress[package_id]["received"] = self._progress[package_id]["total"]
  76. try:
  77. with tempfile.NamedTemporaryFile(mode = "wb+", suffix = ".curapackage", delete = False) as temp_file:
  78. bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
  79. while bytes_read:
  80. temp_file.write(bytes_read)
  81. bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
  82. self._app.processEvents()
  83. self._progress[package_id]["file_written"] = temp_file.name
  84. except IOError as e:
  85. Logger.logException("e", "Failed to write downloaded package to temp file", e)
  86. self._onError(package_id)
  87. temp_file.close()
  88. self._checkDone()
  89. def _onProgress(self, package_id: str, rx: int, rt: int) -> None:
  90. self._progress[package_id]["received"] = rx
  91. self._progress[package_id]["total"] = rt
  92. received = 0
  93. total = 0
  94. for item in self._progress.values():
  95. received += item["received"]
  96. total += item["total"]
  97. if total == 0: # Total download size is 0, or unknown, or there are no progress items at all.
  98. self._progress_message.setProgress(100.0)
  99. return
  100. self._progress_message.setProgress(100.0 * (received / total)) # [0 .. 100] %
  101. def _onError(self, package_id: str) -> None:
  102. self._progress.pop(package_id)
  103. self._error.append(package_id)
  104. self._checkDone()
  105. def _checkDone(self) -> bool:
  106. for item in self._progress.values():
  107. if not item["file_written"]:
  108. return False
  109. success_items = {
  110. package_id:
  111. {
  112. "package_path": value["file_written"],
  113. "icon_url": value["package_model"]["icon_url"]
  114. }
  115. for package_id, value in self._progress.items()
  116. }
  117. error_items = [package_id for package_id in self._error]
  118. self._progress_message.hide()
  119. self.done.emit(success_items, error_items)
  120. return True