| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208 |
- """Tornado handlers for nbconvert."""
- # Copyright (c) Jupyter Development Team.
- # Distributed under the terms of the Modified BSD License.
- import io
- import os
- import sys
- import zipfile
- from anyio.to_thread import run_sync
- from jupyter_core.utils import ensure_async
- from nbformat import from_dict
- from tornado import web
- from tornado.log import app_log
- from jupyter_server.auth.decorator import authorized
- from ..base.handlers import FilesRedirectHandler, JupyterHandler, path_regex
- AUTH_RESOURCE = "nbconvert"
- # datetime.strftime date format for jupyter
- # inlined from ipython_genutils
- if sys.platform == "win32":
- date_format = "%B %d, %Y"
- else:
- date_format = "%B %-d, %Y"
- def find_resource_files(output_files_dir):
- """Find the resource files in a directory."""
- files = []
- for dirpath, _, filenames in os.walk(output_files_dir):
- files.extend([os.path.join(dirpath, f) for f in filenames])
- return files
- def respond_zip(handler, name, output, resources):
- """Zip up the output and resource files and respond with the zip file.
- Returns True if it has served a zip file, False if there are no resource
- files, in which case we serve the plain output file.
- """
- # Check if we have resource files we need to zip
- output_files = resources.get("outputs", None)
- if not output_files:
- return False
- # Headers
- zip_filename = os.path.splitext(name)[0] + ".zip"
- handler.set_attachment_header(zip_filename)
- handler.set_header("Content-Type", "application/zip")
- handler.set_header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
- # Prepare the zip file
- buffer = io.BytesIO()
- zipf = zipfile.ZipFile(buffer, mode="w", compression=zipfile.ZIP_DEFLATED)
- output_filename = os.path.splitext(name)[0] + resources["output_extension"]
- zipf.writestr(output_filename, output.encode("utf-8"))
- for filename, data in output_files.items():
- zipf.writestr(os.path.basename(filename), data)
- zipf.close()
- handler.finish(buffer.getvalue())
- return True
- def get_exporter(format, **kwargs):
- """get an exporter, raising appropriate errors"""
- # if this fails, will raise 500
- try:
- from nbconvert.exporters.base import get_exporter
- except ImportError as e:
- raise web.HTTPError(500, "Could not import nbconvert: %s" % e) from e
- try:
- exporter = get_exporter(format)
- except KeyError as e:
- # should this be 400?
- raise web.HTTPError(404, "No exporter for format: %s" % format) from e
- try:
- return exporter(**kwargs)
- except Exception as e:
- app_log.exception("Could not construct Exporter: %s", exporter)
- raise web.HTTPError(500, "Could not construct Exporter: %s" % e) from e
- class NbconvertFileHandler(JupyterHandler):
- """An nbconvert file handler."""
- auth_resource = AUTH_RESOURCE
- SUPPORTED_METHODS = ("GET",)
- @web.authenticated
- @authorized
- async def get(self, format, path):
- """Get a notebook file in a desired format."""
- self.check_xsrf_cookie()
- exporter = get_exporter(format, config=self.config, log=self.log)
- path = path.strip("/")
- # If the notebook relates to a real file (default contents manager),
- # give its path to nbconvert.
- if hasattr(self.contents_manager, "_get_os_path"):
- os_path = self.contents_manager._get_os_path(path)
- ext_resources_dir, basename = os.path.split(os_path)
- else:
- ext_resources_dir = None
- model = await ensure_async(self.contents_manager.get(path=path))
- name = model["name"]
- if model["type"] != "notebook":
- # not a notebook, redirect to files
- return FilesRedirectHandler.redirect_to_files(self, path)
- nb = model["content"]
- self.set_header("Last-Modified", model["last_modified"])
- # create resources dictionary
- mod_date = model["last_modified"].strftime(date_format)
- nb_title = os.path.splitext(name)[0]
- resource_dict = {
- "metadata": {"name": nb_title, "modified_date": mod_date},
- "config_dir": self.application.settings["config_dir"],
- }
- if ext_resources_dir:
- resource_dict["metadata"]["path"] = ext_resources_dir
- # Exporting can take a while, delegate to a thread so we don't block the event loop
- try:
- output, resources = await run_sync(
- lambda: exporter.from_notebook_node(nb, resources=resource_dict)
- )
- except Exception as e:
- self.log.exception("nbconvert failed: %r", e)
- raise web.HTTPError(500, "nbconvert failed: %s" % e) from e
- if respond_zip(self, name, output, resources):
- return None
- # Force download if requested
- if self.get_argument("download", "false").lower() == "true":
- filename = os.path.splitext(name)[0] + resources["output_extension"]
- self.set_attachment_header(filename)
- # MIME type
- if exporter.output_mimetype:
- self.set_header("Content-Type", "%s; charset=utf-8" % exporter.output_mimetype)
- self.set_header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
- self.finish(output)
- class NbconvertPostHandler(JupyterHandler):
- """An nbconvert post handler."""
- SUPPORTED_METHODS = ("POST",)
- auth_resource = AUTH_RESOURCE
- @web.authenticated
- @authorized
- async def post(self, format):
- """Convert a notebook file to a desired format."""
- exporter = get_exporter(format, config=self.config)
- model = self.get_json_body()
- assert model is not None
- name = model.get("name", "notebook.ipynb")
- nbnode = from_dict(model["content"])
- try:
- output, resources = await run_sync(
- lambda: exporter.from_notebook_node(
- nbnode,
- resources={
- "metadata": {"name": name[: name.rfind(".")]},
- "config_dir": self.application.settings["config_dir"],
- },
- )
- )
- except Exception as e:
- raise web.HTTPError(500, "nbconvert failed: %s" % e) from e
- if respond_zip(self, name, output, resources):
- return
- # MIME type
- if exporter.output_mimetype:
- self.set_header("Content-Type", "%s; charset=utf-8" % exporter.output_mimetype)
- self.finish(output)
- # -----------------------------------------------------------------------------
- # URL to handler mappings
- # -----------------------------------------------------------------------------
- _format_regex = r"(?P<format>\w+)"
- default_handlers = [
- (r"/nbconvert/%s" % _format_regex, NbconvertPostHandler),
- (rf"/nbconvert/{_format_regex}{path_regex}", NbconvertFileHandler),
- ]
|