123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325 |
- """Tornado handlers for the contents web service.
- Preliminary documentation at https://github.com/ipython/ipython/wiki/IPEP-27%3A-Contents-Service
- """
- # Copyright (c) Jupyter Development Team.
- # Distributed under the terms of the Modified BSD License.
- import json
- from tornado import gen, web
- from notebook.utils import url_path_join, url_escape
- from jupyter_client.jsonutil import date_default
- from notebook.base.handlers import (
- IPythonHandler, APIHandler, path_regex,
- )
- def validate_model(model, expect_content):
- """
- Validate a model returned by a ContentsManager method.
- If expect_content is True, then we expect non-null entries for 'content'
- and 'format'.
- """
- required_keys = {
- "name",
- "path",
- "type",
- "writable",
- "created",
- "last_modified",
- "mimetype",
- "content",
- "format",
- }
- missing = required_keys - set(model.keys())
- if missing:
- raise web.HTTPError(
- 500,
- u"Missing Model Keys: {missing}".format(missing=missing),
- )
- maybe_none_keys = ['content', 'format']
- if expect_content:
- errors = [key for key in maybe_none_keys if model[key] is None]
- if errors:
- raise web.HTTPError(
- 500,
- u"Keys unexpectedly None: {keys}".format(keys=errors),
- )
- else:
- errors = {
- key: model[key]
- for key in maybe_none_keys
- if model[key] is not None
- }
- if errors:
- raise web.HTTPError(
- 500,
- u"Keys unexpectedly not None: {keys}".format(keys=errors),
- )
- class ContentsHandler(APIHandler):
- def location_url(self, path):
- """Return the full URL location of a file.
- Parameters
- ----------
- path : unicode
- The API path of the file, such as "foo/bar.txt".
- """
- return url_path_join(
- self.base_url, 'api', 'contents', url_escape(path)
- )
- def _finish_model(self, model, location=True):
- """Finish a JSON request with a model, setting relevant headers, etc."""
- if location:
- location = self.location_url(model['path'])
- self.set_header('Location', location)
- self.set_header('Last-Modified', model['last_modified'])
- self.set_header('Content-Type', 'application/json')
- self.finish(json.dumps(model, default=date_default))
- @web.authenticated
- @gen.coroutine
- def get(self, path=''):
- """Return a model for a file or directory.
- A directory model contains a list of models (without content)
- of the files and directories it contains.
- """
- path = path or ''
- type = self.get_query_argument('type', default=None)
- if type not in {None, 'directory', 'file', 'notebook'}:
- raise web.HTTPError(400, u'Type %r is invalid' % type)
- format = self.get_query_argument('format', default=None)
- if format not in {None, 'text', 'base64'}:
- raise web.HTTPError(400, u'Format %r is invalid' % format)
- content = self.get_query_argument('content', default='1')
- if content not in {'0', '1'}:
- raise web.HTTPError(400, u'Content %r is invalid' % content)
- content = int(content)
-
- model = yield gen.maybe_future(self.contents_manager.get(
- path=path, type=type, format=format, content=content,
- ))
- validate_model(model, expect_content=content)
- self._finish_model(model, location=False)
- @web.authenticated
- @gen.coroutine
- def patch(self, path=''):
- """PATCH renames a file or directory without re-uploading content."""
- cm = self.contents_manager
- model = self.get_json_body()
- if model is None:
- raise web.HTTPError(400, u'JSON body missing')
- model = yield gen.maybe_future(cm.update(model, path))
- validate_model(model, expect_content=False)
- self._finish_model(model)
-
- @gen.coroutine
- def _copy(self, copy_from, copy_to=None):
- """Copy a file, optionally specifying a target directory."""
- self.log.info(u"Copying {copy_from} to {copy_to}".format(
- copy_from=copy_from,
- copy_to=copy_to or '',
- ))
- model = yield gen.maybe_future(self.contents_manager.copy(copy_from, copy_to))
- self.set_status(201)
- validate_model(model, expect_content=False)
- self._finish_model(model)
- @gen.coroutine
- def _upload(self, model, path):
- """Handle upload of a new file to path"""
- self.log.info(u"Uploading file to %s", path)
- model = yield gen.maybe_future(self.contents_manager.new(model, path))
- self.set_status(201)
- validate_model(model, expect_content=False)
- self._finish_model(model)
-
- @gen.coroutine
- def _new_untitled(self, path, type='', ext=''):
- """Create a new, empty untitled entity"""
- self.log.info(u"Creating new %s in %s", type or 'file', path)
- model = yield gen.maybe_future(self.contents_manager.new_untitled(path=path, type=type, ext=ext))
- self.set_status(201)
- validate_model(model, expect_content=False)
- self._finish_model(model)
-
- @gen.coroutine
- def _save(self, model, path):
- """Save an existing file."""
- chunk = model.get("chunk", None)
- if not chunk or chunk == -1: # Avoid tedious log information
- self.log.info(u"Saving file at %s", path)
- model = yield gen.maybe_future(self.contents_manager.save(model, path))
- validate_model(model, expect_content=False)
- self._finish_model(model)
- @web.authenticated
- @gen.coroutine
- def post(self, path=''):
- """Create a new file in the specified path.
- POST creates new files. The server always decides on the name.
- POST /api/contents/path
- New untitled, empty file or directory.
- POST /api/contents/path
- with body {"copy_from" : "/path/to/OtherNotebook.ipynb"}
- New copy of OtherNotebook in path
- """
- cm = self.contents_manager
- if cm.file_exists(path):
- raise web.HTTPError(400, "Cannot POST to files, use PUT instead.")
- if not cm.dir_exists(path):
- raise web.HTTPError(404, "No such directory: %s" % path)
- model = self.get_json_body()
- if model is not None:
- copy_from = model.get('copy_from')
- ext = model.get('ext', '')
- type = model.get('type', '')
- if copy_from:
- yield self._copy(copy_from, path)
- else:
- yield self._new_untitled(path, type=type, ext=ext)
- else:
- yield self._new_untitled(path)
- @web.authenticated
- @gen.coroutine
- def put(self, path=''):
- """Saves the file in the location specified by name and path.
- PUT is very similar to POST, but the requester specifies the name,
- whereas with POST, the server picks the name.
- PUT /api/contents/path/Name.ipynb
- Save notebook at ``path/Name.ipynb``. Notebook structure is specified
- in `content` key of JSON request body. If content is not specified,
- create a new empty notebook.
- """
- model = self.get_json_body()
- if model:
- if model.get('copy_from'):
- raise web.HTTPError(400, "Cannot copy with PUT, only POST")
- exists = yield gen.maybe_future(self.contents_manager.file_exists(path))
- if exists:
- yield gen.maybe_future(self._save(model, path))
- else:
- yield gen.maybe_future(self._upload(model, path))
- else:
- yield gen.maybe_future(self._new_untitled(path))
- @web.authenticated
- @gen.coroutine
- def delete(self, path=''):
- """delete a file in the given path"""
- cm = self.contents_manager
- self.log.warning('delete %s', path)
- yield gen.maybe_future(cm.delete(path))
- self.set_status(204)
- self.finish()
- class CheckpointsHandler(APIHandler):
- @web.authenticated
- @gen.coroutine
- def get(self, path=''):
- """get lists checkpoints for a file"""
- cm = self.contents_manager
- checkpoints = yield gen.maybe_future(cm.list_checkpoints(path))
- data = json.dumps(checkpoints, default=date_default)
- self.finish(data)
- @web.authenticated
- @gen.coroutine
- def post(self, path=''):
- """post creates a new checkpoint"""
- cm = self.contents_manager
- checkpoint = yield gen.maybe_future(cm.create_checkpoint(path))
- data = json.dumps(checkpoint, default=date_default)
- location = url_path_join(self.base_url, 'api/contents',
- url_escape(path), 'checkpoints', url_escape(checkpoint['id']))
- self.set_header('Location', location)
- self.set_status(201)
- self.finish(data)
- class ModifyCheckpointsHandler(APIHandler):
- @web.authenticated
- @gen.coroutine
- def post(self, path, checkpoint_id):
- """post restores a file from a checkpoint"""
- cm = self.contents_manager
- yield gen.maybe_future(cm.restore_checkpoint(checkpoint_id, path))
- self.set_status(204)
- self.finish()
- @web.authenticated
- @gen.coroutine
- def delete(self, path, checkpoint_id):
- """delete clears a checkpoint for a given file"""
- cm = self.contents_manager
- yield gen.maybe_future(cm.delete_checkpoint(checkpoint_id, path))
- self.set_status(204)
- self.finish()
- class NotebooksRedirectHandler(IPythonHandler):
- """Redirect /api/notebooks to /api/contents"""
- SUPPORTED_METHODS = ('GET', 'PUT', 'PATCH', 'POST', 'DELETE')
- def get(self, path):
- self.log.warning("/api/notebooks is deprecated, use /api/contents")
- self.redirect(url_path_join(
- self.base_url,
- 'api/contents',
- path
- ))
- put = patch = post = delete = get
- class TrustNotebooksHandler(IPythonHandler):
- """ Handles trust/signing of notebooks """
- @web.authenticated
- @gen.coroutine
- def post(self,path=''):
- cm = self.contents_manager
- yield gen.maybe_future(cm.trust_notebook(path))
- self.set_status(201)
- self.finish()
- #-----------------------------------------------------------------------------
- # URL to handler mappings
- #-----------------------------------------------------------------------------
- _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
- default_handlers = [
- (r"/api/contents%s/checkpoints" % path_regex, CheckpointsHandler),
- (r"/api/contents%s/checkpoints/%s" % (path_regex, _checkpoint_id_regex),
- ModifyCheckpointsHandler),
- (r"/api/contents%s/trust" % path_regex, TrustNotebooksHandler),
- (r"/api/contents%s" % path_regex, ContentsHandler),
- (r"/api/notebooks/?(.*)", NotebooksRedirectHandler),
- ]
|