DFFileUploader.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. # Copyright (c) 2021 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from PyQt6.QtNetwork import QNetworkRequest, QNetworkReply
  4. from typing import Callable, Any, cast, Optional, Union
  5. from UM.Logger import Logger
  6. from UM.TaskManagement.HttpRequestManager import HttpRequestManager
  7. from .DFLibraryFileUploadResponse import DFLibraryFileUploadResponse
  8. from .DFPrintJobUploadResponse import DFPrintJobUploadResponse
  9. class DFFileUploader:
  10. """Class responsible for uploading meshes to the the digital factory library in separate requests."""
  11. # The maximum amount of times to retry if the server returns one of the RETRY_HTTP_CODES
  12. MAX_RETRIES = 10
  13. # The HTTP codes that should trigger a retry.
  14. RETRY_HTTP_CODES = {500, 502, 503, 504}
  15. def __init__(self,
  16. http: HttpRequestManager,
  17. df_file: Union[DFLibraryFileUploadResponse, DFPrintJobUploadResponse],
  18. data: bytes,
  19. on_finished: Callable[[str], Any],
  20. on_success: Callable[[str], Any],
  21. on_progress: Callable[[str, int], Any],
  22. on_error: Callable[[str, "QNetworkReply", "QNetworkReply.NetworkError"], Any]
  23. ) -> None:
  24. """Creates a mesh upload object.
  25. :param http: The network access manager that will handle the HTTP requests.
  26. :param df_file: The file response that was received by the Digital Factory after registering the upload.
  27. :param data: The mesh bytes to be uploaded.
  28. :param on_finished: The method to be called when done.
  29. :param on_success: The method to be called when the upload is successful.
  30. :param on_progress: The method to be called when the progress changes (receives a percentage 0-100).
  31. :param on_error: The method to be called when an error occurs.
  32. """
  33. self._http: HttpRequestManager = http
  34. self._df_file: Union[DFLibraryFileUploadResponse, DFPrintJobUploadResponse] = df_file
  35. self._file_name = ""
  36. if isinstance(self._df_file, DFLibraryFileUploadResponse):
  37. self._file_name = self._df_file.file_name
  38. elif isinstance(self._df_file, DFPrintJobUploadResponse):
  39. if self._df_file.job_name is not None:
  40. self._file_name = self._df_file.job_name
  41. else:
  42. self._file_name = ""
  43. else:
  44. raise TypeError("Incorrect input type")
  45. self._data: bytes = data
  46. self._on_finished = on_finished
  47. self._on_success = on_success
  48. self._on_progress = on_progress
  49. self._on_error = on_error
  50. self._retries = 0
  51. self._finished = False
  52. def start(self) -> None:
  53. """Starts uploading the mesh."""
  54. if self._finished:
  55. # reset state.
  56. self._retries = 0
  57. self._finished = False
  58. self._upload()
  59. def stop(self):
  60. """Stops uploading the mesh, marking it as finished."""
  61. Logger.log("i", "Finished uploading")
  62. self._finished = True # Signal to any ongoing retries that we should stop retrying.
  63. self._on_finished(self._file_name)
  64. def _upload(self) -> None:
  65. """
  66. Uploads the file to the Digital Factory Library project
  67. """
  68. if self._finished:
  69. raise ValueError("The upload is already finished")
  70. if isinstance(self._df_file, DFLibraryFileUploadResponse):
  71. Logger.log("i", "Uploading Cura project file '{file_name}' via link '{upload_url}'".format(file_name = self._df_file.file_name, upload_url = self._df_file.upload_url))
  72. elif isinstance(self._df_file, DFPrintJobUploadResponse):
  73. Logger.log("i", "Uploading Cura print file '{file_name}' via link '{upload_url}'".format(file_name = self._df_file.job_name, upload_url = self._df_file.upload_url))
  74. self._http.put(
  75. url = cast(str, self._df_file.upload_url),
  76. headers_dict = {"Content-Type": cast(str, self._df_file.content_type)},
  77. data = self._data,
  78. callback = self._onUploadFinished,
  79. error_callback = self._onUploadError,
  80. upload_progress_callback = self._onUploadProgressChanged
  81. )
  82. def _onUploadProgressChanged(self, bytes_sent: int, bytes_total: int) -> None:
  83. """Handles an update to the upload progress
  84. :param bytes_sent: The amount of bytes sent in the current request.
  85. :param bytes_total: The amount of bytes to send in the current request.
  86. """
  87. Logger.debug("Cloud upload progress %s / %s", bytes_sent, bytes_total)
  88. if bytes_total:
  89. self._on_progress(self._file_name, int(bytes_sent / len(self._data) * 100))
  90. def _onUploadError(self, reply: QNetworkReply, error: QNetworkReply.NetworkError) -> None:
  91. """Handles an error uploading."""
  92. body = bytes(reply.peek(reply.bytesAvailable())).decode()
  93. Logger.log("e", "Received error while uploading: %s", body)
  94. self._on_error(self._file_name, reply, error)
  95. self.stop()
  96. def _onUploadFinished(self, reply: QNetworkReply) -> None:
  97. """
  98. Checks whether a chunk of data was uploaded successfully, starting the next chunk if needed.
  99. """
  100. Logger.log("i", "Finished callback %s %s",
  101. reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute), reply.url().toString())
  102. status_code = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute) # type: Optional[int]
  103. if not status_code:
  104. Logger.log("e", "Reply contained no status code.")
  105. self._onUploadError(reply, None)
  106. return
  107. # check if we should retry the last chunk
  108. if self._retries < self.MAX_RETRIES and status_code in self.RETRY_HTTP_CODES:
  109. self._retries += 1
  110. Logger.log("i", "Retrying %s/%s request %s", self._retries, self.MAX_RETRIES, reply.url().toString())
  111. try:
  112. self._upload()
  113. except ValueError: # Asynchronously it could have completed in the meanwhile.
  114. pass
  115. return
  116. # Http codes that are not to be retried are assumed to be errors.
  117. if status_code > 308:
  118. self._onUploadError(reply, None)
  119. return
  120. Logger.log("d", "status_code: %s, Headers: %s, body: %s", status_code,
  121. [bytes(header).decode() for header in reply.rawHeaderList()], bytes(reply.readAll()).decode())
  122. self._on_success(self._file_name)
  123. self.stop()