largefilemanager.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import base64
  2. import os
  3. from anyio.to_thread import run_sync
  4. from tornado import web
  5. from jupyter_server.services.contents.filemanager import (
  6. AsyncFileContentsManager,
  7. FileContentsManager,
  8. )
  9. class LargeFileManager(FileContentsManager):
  10. """Handle large file upload."""
  11. def save(self, model, path=""):
  12. """Save the file model and return the model with no content."""
  13. chunk = model.get("chunk", None)
  14. if chunk is not None:
  15. path = path.strip("/")
  16. if chunk == 1:
  17. self.run_pre_save_hooks(model=model, path=path)
  18. if "type" not in model:
  19. raise web.HTTPError(400, "No file type provided")
  20. if model["type"] != "file":
  21. raise web.HTTPError(
  22. 400,
  23. 'File type "{}" is not supported for large file transfer'.format(model["type"]),
  24. )
  25. if "content" not in model and model["type"] != "directory":
  26. raise web.HTTPError(400, "No file content provided")
  27. os_path = self._get_os_path(path)
  28. if chunk == -1:
  29. self.log.debug(f"Saving last chunk of file {os_path}")
  30. else:
  31. self.log.debug(f"Saving chunk {chunk} of file {os_path}")
  32. try:
  33. if chunk == 1:
  34. super()._save_file(os_path, model["content"], model.get("format"))
  35. else:
  36. self._save_large_file(os_path, model["content"], model.get("format"))
  37. except web.HTTPError:
  38. raise
  39. except Exception as e:
  40. self.log.error("Error while saving file: %s %s", path, e, exc_info=True)
  41. raise web.HTTPError(500, f"Unexpected error while saving file: {path} {e}") from e
  42. model = self.get(path, content=False)
  43. # Last chunk
  44. if chunk == -1:
  45. self.run_post_save_hooks(model=model, os_path=os_path)
  46. self.emit(data={"action": "save", "path": path})
  47. return model
  48. else:
  49. return super().save(model, path)
  50. def _save_large_file(self, os_path, content, format):
  51. """Save content of a generic file."""
  52. if format not in {"text", "base64"}:
  53. raise web.HTTPError(
  54. 400,
  55. "Must specify format of file contents as 'text' or 'base64'",
  56. )
  57. try:
  58. if format == "text":
  59. bcontent = content.encode("utf8")
  60. else:
  61. b64_bytes = content.encode("ascii")
  62. bcontent = base64.b64decode(b64_bytes)
  63. except Exception as e:
  64. raise web.HTTPError(400, f"Encoding error saving {os_path}: {e}") from e
  65. with self.perm_to_403(os_path):
  66. if os.path.islink(os_path):
  67. os_path = os.path.join(os.path.dirname(os_path), os.readlink(os_path))
  68. with open(os_path, "ab") as f:
  69. f.write(bcontent)
  70. class AsyncLargeFileManager(AsyncFileContentsManager):
  71. """Handle large file upload asynchronously"""
  72. async def save(self, model, path=""):
  73. """Save the file model and return the model with no content."""
  74. chunk = model.get("chunk", None)
  75. if chunk is not None:
  76. path = path.strip("/")
  77. if chunk == 1:
  78. self.run_pre_save_hooks(model=model, path=path)
  79. if "type" not in model:
  80. raise web.HTTPError(400, "No file type provided")
  81. if model["type"] != "file":
  82. raise web.HTTPError(
  83. 400,
  84. 'File type "{}" is not supported for large file transfer'.format(model["type"]),
  85. )
  86. if "content" not in model and model["type"] != "directory":
  87. raise web.HTTPError(400, "No file content provided")
  88. os_path = self._get_os_path(path)
  89. if chunk == -1:
  90. self.log.debug(f"Saving last chunk of file {os_path}")
  91. else:
  92. self.log.debug(f"Saving chunk {chunk} of file {os_path}")
  93. try:
  94. if chunk == 1:
  95. await super()._save_file(os_path, model["content"], model.get("format"))
  96. else:
  97. await self._save_large_file(os_path, model["content"], model.get("format"))
  98. except web.HTTPError:
  99. raise
  100. except Exception as e:
  101. self.log.error("Error while saving file: %s %s", path, e, exc_info=True)
  102. raise web.HTTPError(500, f"Unexpected error while saving file: {path} {e}") from e
  103. model = await self.get(path, content=False)
  104. # Last chunk
  105. if chunk == -1:
  106. self.run_post_save_hooks(model=model, os_path=os_path)
  107. self.emit(data={"action": "save", "path": path})
  108. return model
  109. else:
  110. return await super().save(model, path)
  111. async def _save_large_file(self, os_path, content, format):
  112. """Save content of a generic file."""
  113. if format not in {"text", "base64"}:
  114. raise web.HTTPError(
  115. 400,
  116. "Must specify format of file contents as 'text' or 'base64'",
  117. )
  118. try:
  119. if format == "text":
  120. bcontent = content.encode("utf8")
  121. else:
  122. b64_bytes = content.encode("ascii")
  123. bcontent = base64.b64decode(b64_bytes)
  124. except Exception as e:
  125. raise web.HTTPError(400, f"Encoding error saving {os_path}: {e}") from e
  126. with self.perm_to_403(os_path):
  127. if os.path.islink(os_path):
  128. os_path = os.path.join(os.path.dirname(os_path), os.readlink(os_path))
  129. with open(os_path, "ab") as f: # noqa: ASYNC101
  130. await run_sync(f.write, bcontent)