handlers.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. """Tornado handlers for nbconvert."""
  2. # Copyright (c) Jupyter Development Team.
  3. # Distributed under the terms of the Modified BSD License.
  4. import io
  5. import os
  6. import zipfile
  7. from tornado import web, escape
  8. from tornado.log import app_log
  9. from ..base.handlers import (
  10. IPythonHandler, FilesRedirectHandler,
  11. path_regex,
  12. )
  13. from nbformat import from_dict
  14. from ipython_genutils.py3compat import cast_bytes
  15. from ipython_genutils import text
  16. def find_resource_files(output_files_dir):
  17. files = []
  18. for dirpath, dirnames, filenames in os.walk(output_files_dir):
  19. files.extend([os.path.join(dirpath, f) for f in filenames])
  20. return files
  21. def respond_zip(handler, name, output, resources):
  22. """Zip up the output and resource files and respond with the zip file.
  23. Returns True if it has served a zip file, False if there are no resource
  24. files, in which case we serve the plain output file.
  25. """
  26. # Check if we have resource files we need to zip
  27. output_files = resources.get('outputs', None)
  28. if not output_files:
  29. return False
  30. # Headers
  31. zip_filename = os.path.splitext(name)[0] + '.zip'
  32. handler.set_attachment_header(zip_filename)
  33. handler.set_header('Content-Type', 'application/zip')
  34. handler.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
  35. # Prepare the zip file
  36. buffer = io.BytesIO()
  37. zipf = zipfile.ZipFile(buffer, mode='w', compression=zipfile.ZIP_DEFLATED)
  38. output_filename = os.path.splitext(name)[0] + resources['output_extension']
  39. zipf.writestr(output_filename, cast_bytes(output, 'utf-8'))
  40. for filename, data in output_files.items():
  41. zipf.writestr(os.path.basename(filename), data)
  42. zipf.close()
  43. handler.finish(buffer.getvalue())
  44. return True
  45. def get_exporter(format, **kwargs):
  46. """get an exporter, raising appropriate errors"""
  47. # if this fails, will raise 500
  48. try:
  49. from nbconvert.exporters.base import get_exporter
  50. except ImportError as e:
  51. raise web.HTTPError(500, "Could not import nbconvert: %s" % e)
  52. try:
  53. Exporter = get_exporter(format)
  54. except KeyError:
  55. # should this be 400?
  56. raise web.HTTPError(404, u"No exporter for format: %s" % format)
  57. try:
  58. return Exporter(**kwargs)
  59. except Exception as e:
  60. app_log.exception("Could not construct Exporter: %s", Exporter)
  61. raise web.HTTPError(500, "Could not construct Exporter: %s" % e)
  62. class NbconvertFileHandler(IPythonHandler):
  63. SUPPORTED_METHODS = ('GET',)
  64. @property
  65. def content_security_policy(self):
  66. # In case we're serving HTML/SVG, confine any Javascript to a unique
  67. # origin so it can't interact with the notebook server.
  68. return super(NbconvertFileHandler, self).content_security_policy + \
  69. "; sandbox allow-scripts"
  70. @web.authenticated
  71. def get(self, format, path):
  72. exporter = get_exporter(format, config=self.config, log=self.log)
  73. path = path.strip('/')
  74. # If the notebook relates to a real file (default contents manager),
  75. # give its path to nbconvert.
  76. if hasattr(self.contents_manager, '_get_os_path'):
  77. os_path = self.contents_manager._get_os_path(path)
  78. ext_resources_dir, basename = os.path.split(os_path)
  79. else:
  80. ext_resources_dir = None
  81. model = self.contents_manager.get(path=path)
  82. name = model['name']
  83. if model['type'] != 'notebook':
  84. # not a notebook, redirect to files
  85. return FilesRedirectHandler.redirect_to_files(self, path)
  86. nb = model['content']
  87. self.set_header('Last-Modified', model['last_modified'])
  88. # create resources dictionary
  89. mod_date = model['last_modified'].strftime(text.date_format)
  90. nb_title = os.path.splitext(name)[0]
  91. resource_dict = {
  92. "metadata": {
  93. "name": nb_title,
  94. "modified_date": mod_date
  95. },
  96. "config_dir": self.application.settings['config_dir']
  97. }
  98. if ext_resources_dir:
  99. resource_dict['metadata']['path'] = ext_resources_dir
  100. try:
  101. output, resources = exporter.from_notebook_node(
  102. nb,
  103. resources=resource_dict
  104. )
  105. except Exception as e:
  106. self.log.exception("nbconvert failed: %s", e)
  107. raise web.HTTPError(500, "nbconvert failed: %s" % e)
  108. if respond_zip(self, name, output, resources):
  109. return
  110. # Force download if requested
  111. if self.get_argument('download', 'false').lower() == 'true':
  112. filename = os.path.splitext(name)[0] + resources['output_extension']
  113. self.set_attachment_header(filename)
  114. # MIME type
  115. if exporter.output_mimetype:
  116. self.set_header('Content-Type',
  117. '%s; charset=utf-8' % exporter.output_mimetype)
  118. self.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
  119. self.finish(output)
  120. class NbconvertPostHandler(IPythonHandler):
  121. SUPPORTED_METHODS = ('POST',)
  122. @property
  123. def content_security_policy(self):
  124. # In case we're serving HTML/SVG, confine any Javascript to a unique
  125. # origin so it can't interact with the notebook server.
  126. return super(NbconvertPostHandler, self).content_security_policy + \
  127. "; sandbox allow-scripts"
  128. @web.authenticated
  129. def post(self, format):
  130. exporter = get_exporter(format, config=self.config)
  131. model = self.get_json_body()
  132. name = model.get('name', 'notebook.ipynb')
  133. nbnode = from_dict(model['content'])
  134. try:
  135. output, resources = exporter.from_notebook_node(nbnode, resources={
  136. "metadata": {"name": name[:name.rfind('.')],},
  137. "config_dir": self.application.settings['config_dir'],
  138. })
  139. except Exception as e:
  140. raise web.HTTPError(500, "nbconvert failed: %s" % e)
  141. if respond_zip(self, name, output, resources):
  142. return
  143. # MIME type
  144. if exporter.output_mimetype:
  145. self.set_header('Content-Type',
  146. '%s; charset=utf-8' % exporter.output_mimetype)
  147. self.finish(output)
  148. #-----------------------------------------------------------------------------
  149. # URL to handler mappings
  150. #-----------------------------------------------------------------------------
  151. _format_regex = r"(?P<format>\w+)"
  152. default_handlers = [
  153. (r"/nbconvert/%s" % _format_regex, NbconvertPostHandler),
  154. (r"/nbconvert/%s%s" % (_format_regex, path_regex),
  155. NbconvertFileHandler),
  156. ]