RestoreBackupJob.py 3.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. import base64
  2. import hashlib
  3. import threading
  4. from tempfile import NamedTemporaryFile
  5. from typing import Optional, Any, Dict
  6. from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
  7. from UM.Job import Job
  8. from UM.Logger import Logger
  9. from UM.PackageManager import catalog
  10. from UM.TaskManagement.HttpRequestManager import HttpRequestManager
  11. from cura.CuraApplication import CuraApplication
  12. class RestoreBackupJob(Job):
  13. """Downloads a backup and overwrites local configuration with the backup.
  14. When `Job.finished` emits, `restore_backup_error_message` will either be `""` (no error) or an error message
  15. """
  16. DISK_WRITE_BUFFER_SIZE = 512 * 1024
  17. DEFAULT_ERROR_MESSAGE = catalog.i18nc("@info:backup_status", "There was an error trying to restore your backup.")
  18. def __init__(self, backup: Dict[str, Any]) -> None:
  19. """ Create a new restore Job. start the job by calling start()
  20. :param backup: A dict containing a backup spec
  21. """
  22. super().__init__()
  23. self._job_done = threading.Event()
  24. self._backup = backup
  25. self.restore_backup_error_message = ""
  26. def run(self) -> None:
  27. url = self._backup.get("download_url")
  28. assert url is not None
  29. HttpRequestManager.getInstance().get(
  30. url = url,
  31. callback = self._onRestoreRequestCompleted,
  32. error_callback = self._onRestoreRequestCompleted
  33. )
  34. self._job_done.wait() # A job is considered finished when the run function completes
  35. def _onRestoreRequestCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None) -> None:
  36. if not HttpRequestManager.replyIndicatesSuccess(reply, error):
  37. Logger.warning("Requesting backup failed, response code %s while trying to connect to %s",
  38. reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url())
  39. self.restore_backup_error_message = self.DEFAULT_ERROR_MESSAGE
  40. self._job_done.set()
  41. return
  42. # We store the file in a temporary path fist to ensure integrity.
  43. temporary_backup_file = NamedTemporaryFile(delete = False)
  44. with open(temporary_backup_file.name, "wb") as write_backup:
  45. app = CuraApplication.getInstance()
  46. bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
  47. while bytes_read:
  48. write_backup.write(bytes_read)
  49. bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
  50. app.processEvents()
  51. if not self._verifyMd5Hash(temporary_backup_file.name, self._backup.get("md5_hash", "")):
  52. # Don't restore the backup if the MD5 hashes do not match.
  53. # This can happen if the download was interrupted.
  54. Logger.log("w", "Remote and local MD5 hashes do not match, not restoring backup.")
  55. self.restore_backup_error_message = self.DEFAULT_ERROR_MESSAGE
  56. # Tell Cura to place the backup back in the user data folder.
  57. with open(temporary_backup_file.name, "rb") as read_backup:
  58. cura_api = CuraApplication.getInstance().getCuraAPI()
  59. cura_api.backups.restoreBackup(read_backup.read(), self._backup.get("metadata", {}))
  60. self._job_done.set()
  61. @staticmethod
  62. def _verifyMd5Hash(file_path: str, known_hash: str) -> bool:
  63. """Verify the MD5 hash of a file.
  64. :param file_path: Full path to the file.
  65. :param known_hash: The known MD5 hash of the file.
  66. :return: Success or not.
  67. """
  68. with open(file_path, "rb") as read_backup:
  69. local_md5_hash = base64.b64encode(hashlib.md5(read_backup.read()).digest(), altchars = b"_-").decode("utf-8")
  70. return known_hash == local_md5_hash