##// END OF EJS Templates
Move notebook URL fragment regexen into IPython.html.base.handlers
Thomas Kluyver -
Show More
@@ -1,364 +1,372 b''
1 1 """Base Tornado handlers for the notebook.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19
20 20 import functools
21 21 import json
22 22 import logging
23 23 import os
24 24 import stat
25 25 import sys
26 26 import traceback
27 27
28 28 from tornado import web
29 29
30 30 try:
31 31 from tornado.log import app_log
32 32 except ImportError:
33 33 app_log = logging.getLogger()
34 34
35 35 from IPython.config import Application
36 36 from IPython.utils.path import filefind
37 37 from IPython.utils.py3compat import string_types
38 38
39 39 # UF_HIDDEN is a stat flag not defined in the stat module.
40 40 # It is used by BSD to indicate hidden files.
41 41 UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
42 42
43 43 #-----------------------------------------------------------------------------
44 44 # Top-level handlers
45 45 #-----------------------------------------------------------------------------
46 46
47 47 class RequestHandler(web.RequestHandler):
48 48 """RequestHandler with default variable setting."""
49 49
50 50 def render(*args, **kwargs):
51 51 kwargs.setdefault('message', '')
52 52 return web.RequestHandler.render(*args, **kwargs)
53 53
54 54 class AuthenticatedHandler(RequestHandler):
55 55 """A RequestHandler with an authenticated user."""
56 56
57 57 def clear_login_cookie(self):
58 58 self.clear_cookie(self.cookie_name)
59 59
60 60 def get_current_user(self):
61 61 user_id = self.get_secure_cookie(self.cookie_name)
62 62 # For now the user_id should not return empty, but it could eventually
63 63 if user_id == '':
64 64 user_id = 'anonymous'
65 65 if user_id is None:
66 66 # prevent extra Invalid cookie sig warnings:
67 67 self.clear_login_cookie()
68 68 if not self.login_available:
69 69 user_id = 'anonymous'
70 70 return user_id
71 71
72 72 @property
73 73 def cookie_name(self):
74 74 default_cookie_name = 'username-{host}'.format(
75 75 host=self.request.host,
76 76 ).replace(':', '-')
77 77 return self.settings.get('cookie_name', default_cookie_name)
78 78
79 79 @property
80 80 def password(self):
81 81 """our password"""
82 82 return self.settings.get('password', '')
83 83
84 84 @property
85 85 def logged_in(self):
86 86 """Is a user currently logged in?
87 87
88 88 """
89 89 user = self.get_current_user()
90 90 return (user and not user == 'anonymous')
91 91
92 92 @property
93 93 def login_available(self):
94 94 """May a user proceed to log in?
95 95
96 96 This returns True if login capability is available, irrespective of
97 97 whether the user is already logged in or not.
98 98
99 99 """
100 100 return bool(self.settings.get('password', ''))
101 101
102 102
103 103 class IPythonHandler(AuthenticatedHandler):
104 104 """IPython-specific extensions to authenticated handling
105 105
106 106 Mostly property shortcuts to IPython-specific settings.
107 107 """
108 108
109 109 @property
110 110 def config(self):
111 111 return self.settings.get('config', None)
112 112
113 113 @property
114 114 def log(self):
115 115 """use the IPython log by default, falling back on tornado's logger"""
116 116 if Application.initialized():
117 117 return Application.instance().log
118 118 else:
119 119 return app_log
120 120
121 121 @property
122 122 def use_less(self):
123 123 """Use less instead of css in templates"""
124 124 return self.settings.get('use_less', False)
125 125
126 126 #---------------------------------------------------------------
127 127 # URLs
128 128 #---------------------------------------------------------------
129 129
130 130 @property
131 131 def ws_url(self):
132 132 """websocket url matching the current request
133 133
134 134 By default, this is just `''`, indicating that it should match
135 135 the same host, protocol, port, etc.
136 136 """
137 137 return self.settings.get('websocket_url', '')
138 138
139 139 @property
140 140 def mathjax_url(self):
141 141 return self.settings.get('mathjax_url', '')
142 142
143 143 @property
144 144 def base_project_url(self):
145 145 return self.settings.get('base_project_url', '/')
146 146
147 147 @property
148 148 def base_kernel_url(self):
149 149 return self.settings.get('base_kernel_url', '/')
150 150
151 151 #---------------------------------------------------------------
152 152 # Manager objects
153 153 #---------------------------------------------------------------
154 154
155 155 @property
156 156 def kernel_manager(self):
157 157 return self.settings['kernel_manager']
158 158
159 159 @property
160 160 def notebook_manager(self):
161 161 return self.settings['notebook_manager']
162 162
163 163 @property
164 164 def cluster_manager(self):
165 165 return self.settings['cluster_manager']
166 166
167 167 @property
168 168 def session_manager(self):
169 169 return self.settings['session_manager']
170 170
171 171 @property
172 172 def project_dir(self):
173 173 return self.notebook_manager.notebook_dir
174 174
175 175 #---------------------------------------------------------------
176 176 # template rendering
177 177 #---------------------------------------------------------------
178 178
179 179 def get_template(self, name):
180 180 """Return the jinja template object for a given name"""
181 181 return self.settings['jinja2_env'].get_template(name)
182 182
183 183 def render_template(self, name, **ns):
184 184 ns.update(self.template_namespace)
185 185 template = self.get_template(name)
186 186 return template.render(**ns)
187 187
188 188 @property
189 189 def template_namespace(self):
190 190 return dict(
191 191 base_project_url=self.base_project_url,
192 192 base_kernel_url=self.base_kernel_url,
193 193 logged_in=self.logged_in,
194 194 login_available=self.login_available,
195 195 use_less=self.use_less,
196 196 )
197 197
198 198 def get_json_body(self):
199 199 """Return the body of the request as JSON data."""
200 200 if not self.request.body:
201 201 return None
202 202 # Do we need to call body.decode('utf-8') here?
203 203 body = self.request.body.strip().decode(u'utf-8')
204 204 try:
205 205 model = json.loads(body)
206 206 except Exception:
207 207 self.log.debug("Bad JSON: %r", body)
208 208 self.log.error("Couldn't parse JSON", exc_info=True)
209 209 raise web.HTTPError(400, u'Invalid JSON in body of request')
210 210 return model
211 211
212 212
213 213 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
214 214 """static files should only be accessible when logged in"""
215 215
216 216 @web.authenticated
217 217 def get(self, path):
218 218 if os.path.splitext(path)[1] == '.ipynb':
219 219 name = os.path.basename(path)
220 220 self.set_header('Content-Type', 'application/json')
221 221 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
222 222
223 223 return web.StaticFileHandler.get(self, path)
224 224
225 225 def compute_etag(self):
226 226 return None
227 227
228 228 def validate_absolute_path(self, root, absolute_path):
229 229 """Validate and return the absolute path.
230 230
231 231 Requires tornado 3.1
232 232
233 233 Adding to tornado's own handling, forbids the serving of hidden files.
234 234 """
235 235 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
236 236 abs_root = os.path.abspath(root)
237 237 self.forbid_hidden(abs_root, abs_path)
238 238 return abs_path
239 239
240 240 def forbid_hidden(self, absolute_root, absolute_path):
241 241 """Raise 403 if a file is hidden or contained in a hidden directory.
242 242
243 243 Hidden is determined by either name starting with '.'
244 244 or the UF_HIDDEN flag as reported by stat
245 245 """
246 246 inside_root = absolute_path[len(absolute_root):]
247 247 if any(part.startswith('.') for part in inside_root.split(os.sep)):
248 248 raise web.HTTPError(403)
249 249
250 250 # check UF_HIDDEN on any location up to root
251 251 path = absolute_path
252 252 while path and path.startswith(absolute_root) and path != absolute_root:
253 253 st = os.stat(path)
254 254 if getattr(st, 'st_flags', 0) & UF_HIDDEN:
255 255 raise web.HTTPError(403)
256 256 path = os.path.dirname(path)
257 257
258 258 return absolute_path
259 259
260 260
261 261 def json_errors(method):
262 262 """Decorate methods with this to return GitHub style JSON errors.
263 263
264 264 This should be used on any JSON API on any handler method that can raise HTTPErrors.
265 265
266 266 This will grab the latest HTTPError exception using sys.exc_info
267 267 and then:
268 268
269 269 1. Set the HTTP status code based on the HTTPError
270 270 2. Create and return a JSON body with a message field describing
271 271 the error in a human readable form.
272 272 """
273 273 @functools.wraps(method)
274 274 def wrapper(self, *args, **kwargs):
275 275 try:
276 276 result = method(self, *args, **kwargs)
277 277 except web.HTTPError as e:
278 278 status = e.status_code
279 279 message = e.log_message
280 280 self.set_status(e.status_code)
281 281 self.finish(json.dumps(dict(message=message)))
282 282 except Exception:
283 283 self.log.error("Unhandled error in API request", exc_info=True)
284 284 status = 500
285 285 message = "Unknown server error"
286 286 t, value, tb = sys.exc_info()
287 287 self.set_status(status)
288 288 tb_text = ''.join(traceback.format_exception(t, value, tb))
289 289 reply = dict(message=message, traceback=tb_text)
290 290 self.finish(json.dumps(reply))
291 291 else:
292 292 return result
293 293 return wrapper
294 294
295 295
296 296
297 297 #-----------------------------------------------------------------------------
298 298 # File handler
299 299 #-----------------------------------------------------------------------------
300 300
301 301 # to minimize subclass changes:
302 302 HTTPError = web.HTTPError
303 303
304 304 class FileFindHandler(web.StaticFileHandler):
305 305 """subclass of StaticFileHandler for serving files from a search path"""
306 306
307 307 # cache search results, don't search for files more than once
308 308 _static_paths = {}
309 309
310 310 def initialize(self, path, default_filename=None):
311 311 if isinstance(path, string_types):
312 312 path = [path]
313 313
314 314 self.root = tuple(
315 315 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
316 316 )
317 317 self.default_filename = default_filename
318 318
319 319 def compute_etag(self):
320 320 return None
321 321
322 322 @classmethod
323 323 def get_absolute_path(cls, roots, path):
324 324 """locate a file to serve on our static file search path"""
325 325 with cls._lock:
326 326 if path in cls._static_paths:
327 327 return cls._static_paths[path]
328 328 try:
329 329 abspath = os.path.abspath(filefind(path, roots))
330 330 except IOError:
331 331 # IOError means not found
332 332 raise web.HTTPError(404)
333 333
334 334 cls._static_paths[path] = abspath
335 335 return abspath
336 336
337 337 def validate_absolute_path(self, root, absolute_path):
338 338 """check if the file should be served (raises 404, 403, etc.)"""
339 339 for root in self.root:
340 340 if (absolute_path + os.sep).startswith(root):
341 341 break
342 342
343 343 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
344 344
345 345
346 346 class TrailingSlashHandler(web.RequestHandler):
347 347 """Simple redirect handler that strips trailing slashes
348 348
349 349 This should be the first, highest priority handler.
350 350 """
351 351
352 352 SUPPORTED_METHODS = ['GET']
353 353
354 354 def get(self):
355 355 self.redirect(self.request.uri.rstrip('/'))
356 356
357 357 #-----------------------------------------------------------------------------
358 # URL pattern fragments for re-use
359 #-----------------------------------------------------------------------------
360
361 path_regex = r"(?P<path>(?:/.*)*)"
362 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
363 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
364
365 #-----------------------------------------------------------------------------
358 366 # URL to handler mappings
359 367 #-----------------------------------------------------------------------------
360 368
361 369
362 370 default_handlers = [
363 371 (r".*/", TrailingSlashHandler)
364 372 ]
@@ -1,85 +1,83 b''
1 1 import os
2 2
3 3 from tornado import web
4 4
5 from ..base.handlers import IPythonHandler
5 from ..base.handlers import IPythonHandler, notebook_path_regex
6 6 from IPython.nbformat.current import to_notebook_json
7 7 from IPython.nbconvert.exporters.export import exporter_map
8 8 from IPython.utils import tz
9 9
10 10
11 11 def has_resource_files(resources):
12 12 output_files_dir = resources.get('output_files_dir', "")
13 13 return bool(os.path.isdir(output_files_dir) and \
14 14 os.listdir(output_files_dir))
15 15
16 16 class NbconvertFileHandler(IPythonHandler):
17 17
18 18 SUPPORTED_METHODS = ('GET',)
19 19
20 20 @web.authenticated
21 21 def get(self, format, path='', name=None):
22 22 exporter = exporter_map[format](config=self.config)
23 23
24 24 path = path.strip('/')
25 25 os_path = self.notebook_manager.get_os_path(name, path)
26 26 if not os.path.isfile(os_path):
27 27 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
28 28
29 29 info = os.stat(os_path)
30 30 self.set_header('Last-Modified', tz.utcfromtimestamp(info.st_mtime))
31 31
32 32 # Force download if requested
33 33 if self.get_argument('download', 'false').lower() == 'true':
34 34 filename = os.path.splitext(name)[0] + '.' + exporter.file_extension
35 35 self.set_header('Content-Disposition',
36 36 'attachment; filename="%s"' % filename)
37 37
38 38 # MIME type
39 39 if exporter.output_mimetype:
40 40 self.set_header('Content-Type',
41 41 '%s; charset=utf-8' % exporter.output_mimetype)
42 42
43 43 output, resources = exporter.from_filename(os_path)
44 44
45 45 # TODO: If there are resources, combine them into a zip file
46 46 assert not has_resource_files(resources)
47 47
48 48 self.finish(output)
49 49
50 50 class NbconvertPostHandler(IPythonHandler):
51 51 SUPPORTED_METHODS = ('POST',)
52 52
53 53 @web.authenticated
54 54 def post(self, format):
55 55 exporter = exporter_map[format](config=self.config)
56 56
57 57 model = self.get_json_body()
58 58 nbnode = to_notebook_json(model['content'])
59 59
60 60 # MIME type
61 61 if exporter.output_mimetype:
62 62 self.set_header('Content-Type',
63 63 '%s; charset=utf-8' % exporter.output_mimetype)
64 64
65 65 output, resources = exporter.from_notebook_node(nbnode)
66 66
67 67 # TODO: If there are resources, combine them into a zip file
68 68 assert not has_resource_files(resources)
69 69
70 70 self.finish(output)
71 71
72 72 #-----------------------------------------------------------------------------
73 73 # URL to handler mappings
74 74 #-----------------------------------------------------------------------------
75 75
76 76 _format_regex = r"(?P<format>\w+)"
77 _path_regex = r"(?P<path>(?:/.*)*)"
78 _notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
79 _notebook_path_regex = "%s/%s" % (_path_regex, _notebook_name_regex)
77
80 78
81 79 default_handlers = [
82 (r"/nbconvert/%s%s" % (_format_regex, _notebook_path_regex),
80 (r"/nbconvert/%s%s" % (_format_regex, notebook_path_regex),
83 81 NbconvertFileHandler),
84 82 (r"/nbconvert/%s" % _format_regex, NbconvertPostHandler),
85 83 ] No newline at end of file
@@ -1,91 +1,90 b''
1 1 """Tornado handlers for the live notebook view.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 import os
20 20 from tornado import web
21 21 HTTPError = web.HTTPError
22 22
23 from ..base.handlers import IPythonHandler
24 from ..services.notebooks.handlers import _notebook_path_regex, _path_regex
23 from ..base.handlers import IPythonHandler, notebook_path_regex, path_regex
25 24 from ..utils import url_path_join, url_escape
26 25
27 26 #-----------------------------------------------------------------------------
28 27 # Handlers
29 28 #-----------------------------------------------------------------------------
30 29
31 30
32 31 class NotebookHandler(IPythonHandler):
33 32
34 33 @web.authenticated
35 34 def get(self, path='', name=None):
36 35 """get renders the notebook template if a name is given, or
37 36 redirects to the '/files/' handler if the name is not given."""
38 37 path = path.strip('/')
39 38 nbm = self.notebook_manager
40 39 if name is None:
41 40 raise web.HTTPError(500, "This shouldn't be accessible: %s" % self.request.uri)
42 41
43 42 # a .ipynb filename was given
44 43 if not nbm.notebook_exists(name, path):
45 44 raise web.HTTPError(404, u'Notebook does not exist: %s/%s' % (path, name))
46 45 name = url_escape(name)
47 46 path = url_escape(path)
48 47 self.write(self.render_template('notebook.html',
49 48 project=self.project_dir,
50 49 notebook_path=path,
51 50 notebook_name=name,
52 51 kill_kernel=False,
53 52 mathjax_url=self.mathjax_url,
54 53 )
55 54 )
56 55
57 56 class NotebookRedirectHandler(IPythonHandler):
58 57 def get(self, path=''):
59 58 nbm = self.notebook_manager
60 59 if nbm.path_exists(path):
61 60 # it's a *directory*, redirect to /tree
62 61 url = url_path_join(self.base_project_url, 'tree', path)
63 62 else:
64 63 # otherwise, redirect to /files
65 64 if '/files/' in path:
66 65 # redirect without files/ iff it would 404
67 66 # this preserves pre-2.0-style 'files/' links
68 67 # FIXME: this is hardcoded based on notebook_path,
69 68 # but so is the files handler itself,
70 69 # so it should work until both are cleaned up.
71 70 parts = path.split('/')
72 71 files_path = os.path.join(nbm.notebook_dir, *parts)
73 72 self.log.warn("filespath: %s", files_path)
74 73 if not os.path.exists(files_path):
75 74 path = path.replace('/files/', '/', 1)
76 75
77 76 url = url_path_join(self.base_project_url, 'files', path)
78 77 url = url_escape(url)
79 78 self.log.debug("Redirecting %s to %s", self.request.path, url)
80 79 self.redirect(url)
81 80
82 81 #-----------------------------------------------------------------------------
83 82 # URL to handler mappings
84 83 #-----------------------------------------------------------------------------
85 84
86 85
87 86 default_handlers = [
88 (r"/notebooks%s" % _notebook_path_regex, NotebookHandler),
89 (r"/notebooks%s" % _path_regex, NotebookRedirectHandler),
87 (r"/notebooks%s" % notebook_path_regex, NotebookHandler),
88 (r"/notebooks%s" % path_regex, NotebookRedirectHandler),
90 89 ]
91 90
@@ -1,281 +1,280 b''
1 1 """Tornado handlers for the notebooks web service.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 import json
20 20
21 21 from tornado import web
22 22
23 23 from IPython.html.utils import url_path_join, url_escape
24 24 from IPython.utils.jsonutil import date_default
25 25
26 from IPython.html.base.handlers import IPythonHandler, json_errors
26 from IPython.html.base.handlers import (IPythonHandler, json_errors,
27 notebook_path_regex, path_regex,
28 notebook_name_regex)
27 29
28 30 #-----------------------------------------------------------------------------
29 31 # Notebook web service handlers
30 32 #-----------------------------------------------------------------------------
31 33
32 34
33 35 class NotebookHandler(IPythonHandler):
34 36
35 37 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
36 38
37 39 def notebook_location(self, name, path=''):
38 40 """Return the full URL location of a notebook based.
39 41
40 42 Parameters
41 43 ----------
42 44 name : unicode
43 45 The base name of the notebook, such as "foo.ipynb".
44 46 path : unicode
45 47 The URL path of the notebook.
46 48 """
47 49 return url_escape(url_path_join(
48 50 self.base_project_url, 'api', 'notebooks', path, name
49 51 ))
50 52
51 53 def _finish_model(self, model, location=True):
52 54 """Finish a JSON request with a model, setting relevant headers, etc."""
53 55 if location:
54 56 location = self.notebook_location(model['name'], model['path'])
55 57 self.set_header('Location', location)
56 58 self.set_header('Last-Modified', model['last_modified'])
57 59 self.finish(json.dumps(model, default=date_default))
58 60
59 61 @web.authenticated
60 62 @json_errors
61 63 def get(self, path='', name=None):
62 64 """Return a Notebook or list of notebooks.
63 65
64 66 * GET with path and no notebook name lists notebooks in a directory
65 67 * GET with path and notebook name returns notebook JSON
66 68 """
67 69 nbm = self.notebook_manager
68 70 # Check to see if a notebook name was given
69 71 if name is None:
70 72 # List notebooks in 'path'
71 73 notebooks = nbm.list_notebooks(path)
72 74 self.finish(json.dumps(notebooks, default=date_default))
73 75 return
74 76 # get and return notebook representation
75 77 model = nbm.get_notebook_model(name, path)
76 78 self._finish_model(model, location=False)
77 79
78 80 @web.authenticated
79 81 @json_errors
80 82 def patch(self, path='', name=None):
81 83 """PATCH renames a notebook without re-uploading content."""
82 84 nbm = self.notebook_manager
83 85 if name is None:
84 86 raise web.HTTPError(400, u'Notebook name missing')
85 87 model = self.get_json_body()
86 88 if model is None:
87 89 raise web.HTTPError(400, u'JSON body missing')
88 90 model = nbm.update_notebook_model(model, name, path)
89 91 self._finish_model(model)
90 92
91 93 def _copy_notebook(self, copy_from, path, copy_to=None):
92 94 """Copy a notebook in path, optionally specifying the new name.
93 95
94 96 Only support copying within the same directory.
95 97 """
96 98 self.log.info(u"Copying notebook from %s/%s to %s/%s",
97 99 path, copy_from,
98 100 path, copy_to or '',
99 101 )
100 102 model = self.notebook_manager.copy_notebook(copy_from, copy_to, path)
101 103 self.set_status(201)
102 104 self._finish_model(model)
103 105
104 106 def _upload_notebook(self, model, path, name=None):
105 107 """Upload a notebook
106 108
107 109 If name specified, create it in path/name.
108 110 """
109 111 self.log.info(u"Uploading notebook to %s/%s", path, name or '')
110 112 if name:
111 113 model['name'] = name
112 114
113 115 model = self.notebook_manager.create_notebook_model(model, path)
114 116 self.set_status(201)
115 117 self._finish_model(model)
116 118
117 119 def _create_empty_notebook(self, path, name=None):
118 120 """Create an empty notebook in path
119 121
120 122 If name specified, create it in path/name.
121 123 """
122 124 self.log.info(u"Creating new notebook in %s/%s", path, name or '')
123 125 model = {}
124 126 if name:
125 127 model['name'] = name
126 128 model = self.notebook_manager.create_notebook_model(model, path=path)
127 129 self.set_status(201)
128 130 self._finish_model(model)
129 131
130 132 def _save_notebook(self, model, path, name):
131 133 """Save an existing notebook."""
132 134 self.log.info(u"Saving notebook at %s/%s", path, name)
133 135 model = self.notebook_manager.save_notebook_model(model, name, path)
134 136 if model['path'] != path.strip('/') or model['name'] != name:
135 137 # a rename happened, set Location header
136 138 location = True
137 139 else:
138 140 location = False
139 141 self._finish_model(model, location)
140 142
141 143 @web.authenticated
142 144 @json_errors
143 145 def post(self, path='', name=None):
144 146 """Create a new notebook in the specified path.
145 147
146 148 POST creates new notebooks. The server always decides on the notebook name.
147 149
148 150 POST /api/notebooks/path
149 151 New untitled notebook in path. If content specified, upload a
150 152 notebook, otherwise start empty.
151 153 POST /api/notebooks/path?copy=OtherNotebook.ipynb
152 154 New copy of OtherNotebook in path
153 155 """
154 156
155 157 if name is not None:
156 158 raise web.HTTPError(400, "Only POST to directories. Use PUT for full names.")
157 159
158 160 model = self.get_json_body()
159 161
160 162 if model is not None:
161 163 copy_from = model.get('copy_from')
162 164 if copy_from:
163 165 if model.get('content'):
164 166 raise web.HTTPError(400, "Can't upload and copy at the same time.")
165 167 self._copy_notebook(copy_from, path)
166 168 else:
167 169 self._upload_notebook(model, path)
168 170 else:
169 171 self._create_empty_notebook(path)
170 172
171 173 @web.authenticated
172 174 @json_errors
173 175 def put(self, path='', name=None):
174 176 """Saves the notebook in the location specified by name and path.
175 177
176 178 PUT is very similar to POST, but the requester specifies the name,
177 179 whereas with POST, the server picks the name.
178 180
179 181 PUT /api/notebooks/path/Name.ipynb
180 182 Save notebook at ``path/Name.ipynb``. Notebook structure is specified
181 183 in `content` key of JSON request body. If content is not specified,
182 184 create a new empty notebook.
183 185 PUT /api/notebooks/path/Name.ipynb?copy=OtherNotebook.ipynb
184 186 Copy OtherNotebook to Name
185 187 """
186 188 if name is None:
187 189 raise web.HTTPError(400, "Only PUT to full names. Use POST for directories.")
188 190
189 191 model = self.get_json_body()
190 192 if model:
191 193 copy_from = model.get('copy_from')
192 194 if copy_from:
193 195 if model.get('content'):
194 196 raise web.HTTPError(400, "Can't upload and copy at the same time.")
195 197 self._copy_notebook(copy_from, path, name)
196 198 elif self.notebook_manager.notebook_exists(name, path):
197 199 self._save_notebook(model, path, name)
198 200 else:
199 201 self._upload_notebook(model, path, name)
200 202 else:
201 203 self._create_empty_notebook(path, name)
202 204
203 205 @web.authenticated
204 206 @json_errors
205 207 def delete(self, path='', name=None):
206 208 """delete the notebook in the given notebook path"""
207 209 nbm = self.notebook_manager
208 210 nbm.delete_notebook_model(name, path)
209 211 self.set_status(204)
210 212 self.finish()
211 213
212 214
213 215 class NotebookCheckpointsHandler(IPythonHandler):
214 216
215 217 SUPPORTED_METHODS = ('GET', 'POST')
216 218
217 219 @web.authenticated
218 220 @json_errors
219 221 def get(self, path='', name=None):
220 222 """get lists checkpoints for a notebook"""
221 223 nbm = self.notebook_manager
222 224 checkpoints = nbm.list_checkpoints(name, path)
223 225 data = json.dumps(checkpoints, default=date_default)
224 226 self.finish(data)
225 227
226 228 @web.authenticated
227 229 @json_errors
228 230 def post(self, path='', name=None):
229 231 """post creates a new checkpoint"""
230 232 nbm = self.notebook_manager
231 233 checkpoint = nbm.create_checkpoint(name, path)
232 234 data = json.dumps(checkpoint, default=date_default)
233 235 location = url_path_join(self.base_project_url, 'api/notebooks',
234 236 path, name, 'checkpoints', checkpoint['id'])
235 237 self.set_header('Location', url_escape(location))
236 238 self.set_status(201)
237 239 self.finish(data)
238 240
239 241
240 242 class ModifyNotebookCheckpointsHandler(IPythonHandler):
241 243
242 244 SUPPORTED_METHODS = ('POST', 'DELETE')
243 245
244 246 @web.authenticated
245 247 @json_errors
246 248 def post(self, path, name, checkpoint_id):
247 249 """post restores a notebook from a checkpoint"""
248 250 nbm = self.notebook_manager
249 251 nbm.restore_checkpoint(checkpoint_id, name, path)
250 252 self.set_status(204)
251 253 self.finish()
252 254
253 255 @web.authenticated
254 256 @json_errors
255 257 def delete(self, path, name, checkpoint_id):
256 258 """delete clears a checkpoint for a given notebook"""
257 259 nbm = self.notebook_manager
258 260 nbm.delete_checkpoint(checkpoint_id, name, path)
259 261 self.set_status(204)
260 262 self.finish()
261 263
262 264 #-----------------------------------------------------------------------------
263 265 # URL to handler mappings
264 266 #-----------------------------------------------------------------------------
265 267
266 268
267 _path_regex = r"(?P<path>(?:/.*)*)"
268 269 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
269 _notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
270 _notebook_path_regex = "%s/%s" % (_path_regex, _notebook_name_regex)
271 270
272 271 default_handlers = [
273 (r"/api/notebooks%s/checkpoints" % _notebook_path_regex, NotebookCheckpointsHandler),
274 (r"/api/notebooks%s/checkpoints/%s" % (_notebook_path_regex, _checkpoint_id_regex),
272 (r"/api/notebooks%s/checkpoints" % notebook_path_regex, NotebookCheckpointsHandler),
273 (r"/api/notebooks%s/checkpoints/%s" % (notebook_path_regex, _checkpoint_id_regex),
275 274 ModifyNotebookCheckpointsHandler),
276 (r"/api/notebooks%s" % _notebook_path_regex, NotebookHandler),
277 (r"/api/notebooks%s" % _path_regex, NotebookHandler),
275 (r"/api/notebooks%s" % notebook_path_regex, NotebookHandler),
276 (r"/api/notebooks%s" % path_regex, NotebookHandler),
278 277 ]
279 278
280 279
281 280
@@ -1,77 +1,76 b''
1 1 """Tornado handlers for the tree view.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18 import os
19 19
20 20 from tornado import web
21 from ..base.handlers import IPythonHandler
21 from ..base.handlers import IPythonHandler, notebook_path_regex, path_regex
22 22 from ..utils import url_path_join, path2url, url2path, url_escape
23 from ..services.notebooks.handlers import _notebook_path_regex, _path_regex
24 23
25 24 #-----------------------------------------------------------------------------
26 25 # Handlers
27 26 #-----------------------------------------------------------------------------
28 27
29 28
30 29 class TreeHandler(IPythonHandler):
31 30 """Render the tree view, listing notebooks, clusters, etc."""
32 31
33 32 @web.authenticated
34 33 def get(self, path='', name=None):
35 34 path = path.strip('/')
36 35 nbm = self.notebook_manager
37 36 if name is not None:
38 37 # is a notebook, redirect to notebook handler
39 38 url = url_escape(url_path_join(
40 39 self.base_project_url, 'notebooks', path, name
41 40 ))
42 41 self.log.debug("Redirecting %s to %s", self.request.path, url)
43 42 self.redirect(url)
44 43 else:
45 44 if not nbm.path_exists(path=path):
46 45 # no such directory, 404
47 46 raise web.HTTPError(404)
48 47 self.write(self.render_template('tree.html',
49 48 project=self.project_dir,
50 49 tree_url_path=path,
51 50 notebook_path=path,
52 51 ))
53 52
54 53
55 54 class TreeRedirectHandler(IPythonHandler):
56 55 """Redirect a request to the corresponding tree URL"""
57 56
58 57 @web.authenticated
59 58 def get(self, path=''):
60 59 url = url_escape(url_path_join(
61 60 self.base_project_url, 'tree', path.strip('/')
62 61 ))
63 62 self.log.debug("Redirecting %s to %s", self.request.path, url)
64 63 self.redirect(url)
65 64
66 65
67 66 #-----------------------------------------------------------------------------
68 67 # URL to handler mappings
69 68 #-----------------------------------------------------------------------------
70 69
71 70
72 71 default_handlers = [
73 (r"/tree%s" % _notebook_path_regex, TreeHandler),
74 (r"/tree%s" % _path_regex, TreeHandler),
72 (r"/tree%s" % notebook_path_regex, TreeHandler),
73 (r"/tree%s" % path_regex, TreeHandler),
75 74 (r"/tree", TreeHandler),
76 75 (r"/", TreeRedirectHandler),
77 76 ]
General Comments 0
You need to be logged in to leave comments. Login now