You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
101 lines
3.4 KiB
Python
101 lines
3.4 KiB
Python
"""Serve files directly from the ContentsManager."""
|
|
|
|
# Copyright (c) Jupyter Development Team.
|
|
# Distributed under the terms of the Modified BSD License.
|
|
from __future__ import annotations
|
|
|
|
import mimetypes
|
|
from base64 import decodebytes
|
|
from typing import TYPE_CHECKING
|
|
|
|
from jupyter_core.utils import ensure_async
|
|
from tornado import web
|
|
|
|
from jupyter_server.auth.decorator import authorized
|
|
from jupyter_server.base.handlers import JupyterHandler
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Awaitable
|
|
|
|
AUTH_RESOURCE = "contents"
|
|
|
|
|
|
class FilesHandler(JupyterHandler, web.StaticFileHandler):
|
|
"""serve files via ContentsManager
|
|
|
|
Normally used when ContentsManager is not a FileContentsManager.
|
|
|
|
FileContentsManager subclasses use AuthenticatedFilesHandler by default,
|
|
a subclass of StaticFileHandler.
|
|
"""
|
|
|
|
auth_resource = AUTH_RESOURCE
|
|
|
|
@property
|
|
def content_security_policy(self):
|
|
"""The content security policy."""
|
|
# In case we're serving HTML/SVG, confine any Javascript to a unique
|
|
# origin so it can't interact with the notebook server.
|
|
return super().content_security_policy + "; sandbox allow-scripts"
|
|
|
|
@web.authenticated
|
|
@authorized
|
|
def head(self, path: str) -> Awaitable[None] | None: # type:ignore[override]
|
|
"""The head response."""
|
|
self.get(path, include_body=False)
|
|
self.check_xsrf_cookie()
|
|
return self.get(path, include_body=False)
|
|
|
|
@web.authenticated
|
|
@authorized
|
|
async def get(self, path, include_body=True):
|
|
"""Get a file by path."""
|
|
# /files/ requests must originate from the same site
|
|
self.check_xsrf_cookie()
|
|
cm = self.contents_manager
|
|
|
|
if not cm.allow_hidden and await ensure_async(cm.is_hidden(path)):
|
|
self.log.info("Refusing to serve hidden file, via 404 Error")
|
|
raise web.HTTPError(404)
|
|
|
|
path = path.strip("/")
|
|
if "/" in path:
|
|
_, name = path.rsplit("/", 1)
|
|
else:
|
|
name = path
|
|
|
|
model = await ensure_async(cm.get(path, type="file", content=include_body))
|
|
|
|
if self.get_argument("download", None):
|
|
self.set_attachment_header(name)
|
|
|
|
# get mimetype from filename
|
|
if name.lower().endswith(".ipynb"):
|
|
self.set_header("Content-Type", "application/x-ipynb+json")
|
|
else:
|
|
cur_mime, encoding = mimetypes.guess_type(name)
|
|
if cur_mime == "text/plain":
|
|
self.set_header("Content-Type", "text/plain; charset=UTF-8")
|
|
# RFC 6713
|
|
if encoding == "gzip":
|
|
self.set_header("Content-Type", "application/gzip")
|
|
elif encoding is not None:
|
|
self.set_header("Content-Type", "application/octet-stream")
|
|
elif cur_mime is not None:
|
|
self.set_header("Content-Type", cur_mime)
|
|
elif model["format"] == "base64":
|
|
self.set_header("Content-Type", "application/octet-stream")
|
|
else:
|
|
self.set_header("Content-Type", "text/plain; charset=UTF-8")
|
|
|
|
if include_body:
|
|
if model["format"] == "base64":
|
|
b64_bytes = model["content"].encode("ascii")
|
|
self.write(decodebytes(b64_bytes))
|
|
else:
|
|
self.write(model["content"])
|
|
self.flush()
|
|
|
|
|
|
default_handlers: list[JupyterHandler] = []
|