handlers.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. """Serve files directly from the ContentsManager."""
  2. # Copyright (c) Jupyter Development Team.
  3. # Distributed under the terms of the Modified BSD License.
  4. from __future__ import annotations
  5. import mimetypes
  6. from base64 import decodebytes
  7. from typing import TYPE_CHECKING
  8. from jupyter_core.utils import ensure_async
  9. from tornado import web
  10. from jupyter_server.auth.decorator import authorized
  11. from jupyter_server.base.handlers import JupyterHandler
  12. if TYPE_CHECKING:
  13. from collections.abc import Awaitable
  14. AUTH_RESOURCE = "contents"
  15. class FilesHandler(JupyterHandler, web.StaticFileHandler):
  16. """serve files via ContentsManager
  17. Normally used when ContentsManager is not a FileContentsManager.
  18. FileContentsManager subclasses use AuthenticatedFilesHandler by default,
  19. a subclass of StaticFileHandler.
  20. """
  21. auth_resource = AUTH_RESOURCE
  22. @property
  23. def content_security_policy(self):
  24. """The content security policy."""
  25. # In case we're serving HTML/SVG, confine any Javascript to a unique
  26. # origin so it can't interact with the notebook server.
  27. return super().content_security_policy + "; sandbox allow-scripts"
  28. @web.authenticated
  29. @authorized
  30. def head(self, path: str) -> Awaitable[None] | None: # type:ignore[override]
  31. """The head response."""
  32. self.get(path, include_body=False)
  33. self.check_xsrf_cookie()
  34. return self.get(path, include_body=False)
  35. @web.authenticated
  36. @authorized
  37. async def get(self, path, include_body=True):
  38. """Get a file by path."""
  39. # /files/ requests must originate from the same site
  40. self.check_xsrf_cookie()
  41. cm = self.contents_manager
  42. if not cm.allow_hidden and await ensure_async(cm.is_hidden(path)):
  43. self.log.info("Refusing to serve hidden file, via 404 Error")
  44. raise web.HTTPError(404)
  45. path = path.strip("/")
  46. if "/" in path:
  47. _, name = path.rsplit("/", 1)
  48. else:
  49. name = path
  50. model = await ensure_async(cm.get(path, type="file", content=include_body))
  51. if self.get_argument("download", None):
  52. self.set_attachment_header(name)
  53. # get mimetype from filename
  54. if name.lower().endswith(".ipynb"):
  55. self.set_header("Content-Type", "application/x-ipynb+json")
  56. else:
  57. cur_mime, encoding = mimetypes.guess_type(name)
  58. if cur_mime == "text/plain":
  59. self.set_header("Content-Type", "text/plain; charset=UTF-8")
  60. # RFC 6713
  61. if encoding == "gzip":
  62. self.set_header("Content-Type", "application/gzip")
  63. elif encoding is not None:
  64. self.set_header("Content-Type", "application/octet-stream")
  65. elif cur_mime is not None:
  66. self.set_header("Content-Type", cur_mime)
  67. elif model["format"] == "base64":
  68. self.set_header("Content-Type", "application/octet-stream")
  69. else:
  70. self.set_header("Content-Type", "text/plain; charset=UTF-8")
  71. if include_body:
  72. if model["format"] == "base64":
  73. b64_bytes = model["content"].encode("ascii")
  74. self.write(decodebytes(b64_bytes))
  75. else:
  76. self.write(model["content"])
  77. self.flush()
  78. default_handlers: list[JupyterHandler] = []