##// END OF EJS Templates
sync with previous handler changes...
MinRK -
Show More
@@ -1,928 +1,928 b''
1 1 """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) 2008-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 Cookie
20 20 import datetime
21 21 import email.utils
22 22 import hashlib
23 23 import logging
24 24 import mimetypes
25 25 import os
26 26 import stat
27 27 import threading
28 28 import time
29 29 import uuid
30 30
31 31 from tornado.escape import url_escape
32 32 from tornado import web
33 33 from tornado import websocket
34 34
35 35 try:
36 36 from tornado.log import app_log
37 37 except ImportError:
38 38 app_log = logging.getLogger()
39 39
40 40 from zmq.eventloop import ioloop
41 41 from zmq.utils import jsonapi
42 42
43 43 from IPython.config import Application
44 44 from IPython.external.decorator import decorator
45 45 from IPython.kernel.zmq.session import Session
46 46 from IPython.lib.security import passwd_check
47 47 from IPython.utils.jsonutil import date_default
48 48 from IPython.utils.path import filefind
49 49 from IPython.utils.py3compat import PY3
50 50
51 51 try:
52 52 from docutils.core import publish_string
53 53 except ImportError:
54 54 publish_string = None
55 55
56 56 #-----------------------------------------------------------------------------
57 57 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
58 58 #-----------------------------------------------------------------------------
59 59
60 60 # Google Chrome, as of release 16, changed its websocket protocol number. The
61 61 # parts tornado cares about haven't really changed, so it's OK to continue
62 62 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
63 63 # version as of Oct 30/2011) the version check fails, see the issue report:
64 64
65 65 # https://github.com/facebook/tornado/issues/385
66 66
67 67 # This issue has been fixed in Tornado post 2.1.1:
68 68
69 69 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
70 70
71 71 # Here we manually apply the same patch as above so that users of IPython can
72 72 # continue to work with an officially released Tornado. We make the
73 73 # monkeypatch version check as narrow as possible to limit its effects; once
74 74 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
75 75
76 76 import tornado
77 77
78 78 if tornado.version_info <= (2,1,1):
79 79
80 80 def _execute(self, transforms, *args, **kwargs):
81 81 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
82 82
83 83 self.open_args = args
84 84 self.open_kwargs = kwargs
85 85
86 86 # The difference between version 8 and 13 is that in 8 the
87 87 # client sends a "Sec-Websocket-Origin" header and in 13 it's
88 88 # simply "Origin".
89 89 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
90 90 self.ws_connection = WebSocketProtocol8(self)
91 91 self.ws_connection.accept_connection()
92 92
93 93 elif self.request.headers.get("Sec-WebSocket-Version"):
94 94 self.stream.write(tornado.escape.utf8(
95 95 "HTTP/1.1 426 Upgrade Required\r\n"
96 96 "Sec-WebSocket-Version: 8\r\n\r\n"))
97 97 self.stream.close()
98 98
99 99 else:
100 100 self.ws_connection = WebSocketProtocol76(self)
101 101 self.ws_connection.accept_connection()
102 102
103 103 websocket.WebSocketHandler._execute = _execute
104 104 del _execute
105 105
106 106 #-----------------------------------------------------------------------------
107 107 # Decorator for disabling read-only handlers
108 108 #-----------------------------------------------------------------------------
109 109
110 110 @decorator
111 111 def not_if_readonly(f, self, *args, **kwargs):
112 112 if self.settings.get('read_only', False):
113 113 raise web.HTTPError(403, "Notebook server is read-only")
114 114 else:
115 115 return f(self, *args, **kwargs)
116 116
117 117 @decorator
118 118 def authenticate_unless_readonly(f, self, *args, **kwargs):
119 119 """authenticate this page *unless* readonly view is active.
120 120
121 121 In read-only mode, the notebook list and print view should
122 122 be accessible without authentication.
123 123 """
124 124
125 125 @web.authenticated
126 126 def auth_f(self, *args, **kwargs):
127 127 return f(self, *args, **kwargs)
128 128
129 129 if self.settings.get('read_only', False):
130 130 return f(self, *args, **kwargs)
131 131 else:
132 132 return auth_f(self, *args, **kwargs)
133 133
134 134 def urljoin(*pieces):
135 135 """Join components of url into a relative url
136 136
137 137 Use to prevent double slash when joining subpath
138 138 """
139 139 striped = [s.strip('/') for s in pieces]
140 140 return '/'.join(s for s in striped if s)
141 141
142 142 #-----------------------------------------------------------------------------
143 143 # Top-level handlers
144 144 #-----------------------------------------------------------------------------
145 145
146 146 class RequestHandler(web.RequestHandler):
147 147 """RequestHandler with default variable setting."""
148 148
149 149 def render(*args, **kwargs):
150 150 kwargs.setdefault('message', '')
151 151 return web.RequestHandler.render(*args, **kwargs)
152 152
153 153 class AuthenticatedHandler(RequestHandler):
154 154 """A RequestHandler with an authenticated user."""
155 155
156 156 def clear_login_cookie(self):
157 157 self.clear_cookie(self.cookie_name)
158 158
159 159 def get_current_user(self):
160 160 user_id = self.get_secure_cookie(self.cookie_name)
161 161 # For now the user_id should not return empty, but it could eventually
162 162 if user_id == '':
163 163 user_id = 'anonymous'
164 164 if user_id is None:
165 165 # prevent extra Invalid cookie sig warnings:
166 166 self.clear_login_cookie()
167 167 if not self.read_only and not self.login_available:
168 168 user_id = 'anonymous'
169 169 return user_id
170 170
171 171 @property
172 172 def cookie_name(self):
173 173 return self.settings.get('cookie_name', '')
174 174
175 175 @property
176 176 def password(self):
177 177 """our password"""
178 178 return self.settings.get('password', '')
179 179
180 180 @property
181 181 def logged_in(self):
182 182 """Is a user currently logged in?
183 183
184 184 """
185 185 user = self.get_current_user()
186 186 return (user and not user == 'anonymous')
187 187
188 188 @property
189 189 def login_available(self):
190 190 """May a user proceed to log in?
191 191
192 192 This returns True if login capability is available, irrespective of
193 193 whether the user is already logged in or not.
194 194
195 195 """
196 196 return bool(self.settings.get('password', ''))
197 197
198 198 @property
199 199 def read_only(self):
200 200 """Is the notebook read-only?
201 201
202 202 """
203 203 return self.settings.get('read_only', False)
204 204
205 205
206 206 class IPythonHandler(AuthenticatedHandler):
207 207 """IPython-specific extensions to authenticated handling
208 208
209 209 Mostly property shortcuts to IPython-specific settings.
210 210 """
211 211
212 212 @property
213 213 def config(self):
214 214 return self.settings.get('config', None)
215 215
216 216 @property
217 217 def log(self):
218 218 """use the IPython log by default, falling back on tornado's logger"""
219 219 if Application.initialized():
220 220 return Application.instance().log
221 221 else:
222 222 return app_log
223 223
224 224 @property
225 225 def use_less(self):
226 226 """Use less instead of css in templates"""
227 227 return self.settings.get('use_less', False)
228 228
229 229 #---------------------------------------------------------------
230 230 # URLs
231 231 #---------------------------------------------------------------
232 232
233 233 @property
234 234 def ws_url(self):
235 235 """websocket url matching the current request
236 236
237 237 turns http[s]://host[:port] into
238 238 ws[s]://host[:port]
239 239 """
240 240 proto = self.request.protocol.replace('http', 'ws')
241 241 host = self.settings.get('websocket_host', '')
242 242 # default to config value
243 243 if host == '':
244 244 host = self.request.host # get from request
245 245 return "%s://%s" % (proto, host)
246 246
247 247 @property
248 248 def mathjax_url(self):
249 249 return self.settings.get('mathjax_url', '')
250 250
251 251 @property
252 252 def base_project_url(self):
253 253 return self.settings.get('base_project_url', '/')
254 254
255 255 @property
256 256 def base_kernel_url(self):
257 257 return self.settings.get('base_kernel_url', '/')
258 258
259 259 #---------------------------------------------------------------
260 260 # Manager objects
261 261 #---------------------------------------------------------------
262 262
263 263 @property
264 264 def kernel_manager(self):
265 265 return self.settings['kernel_manager']
266 266
267 267 @property
268 268 def notebook_manager(self):
269 269 return self.settings['notebook_manager']
270 270
271 271 @property
272 272 def cluster_manager(self):
273 273 return self.settings['cluster_manager']
274 274
275 275 @property
276 276 def project(self):
277 277 return self.notebook_manager.notebook_dir
278 278
279 279 #---------------------------------------------------------------
280 280 # template rendering
281 281 #---------------------------------------------------------------
282 282
283 283 def get_template(self, name):
284 284 """Return the jinja template object for a given name"""
285 285 return self.settings['jinja2_env'].get_template(name)
286 286
287 287 def render_template(self, name, **ns):
288 288 ns.update(self.template_namespace)
289 289 template = self.get_template(name)
290 290 return template.render(**ns)
291 291
292 292 @property
293 293 def template_namespace(self):
294 294 return dict(
295 295 base_project_url=self.base_project_url,
296 296 base_kernel_url=self.base_kernel_url,
297 297 read_only=self.read_only,
298 298 logged_in=self.logged_in,
299 299 login_available=self.login_available,
300 300 use_less=self.use_less,
301 301 )
302 302
303 303 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
304 304 """static files should only be accessible when logged in"""
305 305
306 306 @authenticate_unless_readonly
307 307 def get(self, path):
308 308 return web.StaticFileHandler.get(self, path)
309 309
310 310
311 311 class ProjectDashboardHandler(IPythonHandler):
312 312
313 313 @authenticate_unless_readonly
314 314 def get(self):
315 315 self.write(self.render_template('projectdashboard.html',
316 316 project=self.project,
317 317 project_component=self.project.split('/'),
318 318 ))
319 319
320 320
321 321 class LoginHandler(IPythonHandler):
322 322
323 323 def _render(self, message=None):
324 324 self.write(self.render_template('login.html',
325 325 next=url_escape(self.get_argument('next', default=self.base_project_url)),
326 326 message=message,
327 327 ))
328 328
329 329 def get(self):
330 330 if self.current_user:
331 331 self.redirect(self.get_argument('next', default=self.base_project_url))
332 332 else:
333 333 self._render()
334 334
335 335 def post(self):
336 336 pwd = self.get_argument('password', default=u'')
337 337 if self.login_available:
338 338 if passwd_check(self.password, pwd):
339 339 self.set_secure_cookie(self.cookie_name, str(uuid.uuid4()))
340 340 else:
341 341 self._render(message={'error': 'Invalid password'})
342 342 return
343 343
344 344 self.redirect(self.get_argument('next', default=self.base_project_url))
345 345
346 346
347 347 class LogoutHandler(IPythonHandler):
348 348
349 349 def get(self):
350 350 self.clear_login_cookie()
351 351 if self.login_available:
352 352 message = {'info': 'Successfully logged out.'}
353 353 else:
354 354 message = {'warning': 'Cannot log out. Notebook authentication '
355 355 'is disabled.'}
356 356 self.write(self.render_template('logout.html',
357 357 message=message))
358 358
359 359
360 360 class NewHandler(IPythonHandler):
361 361
362 362 @web.authenticated
363 363 def get(self):
364 364 notebook_id = self.notebook_manager.new_notebook()
365 365 self.redirect('/' + urljoin(self.base_project_url, notebook_id))
366 366
367 367 class NamedNotebookHandler(IPythonHandler):
368 368
369 369 @authenticate_unless_readonly
370 370 def get(self, notebook_id):
371 371 nbm = self.notebook_manager
372 372 if not nbm.notebook_exists(notebook_id):
373 373 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
374 374 self.write(self.render_template('notebook.html',
375 375 project=self.project,
376 376 notebook_id=notebook_id,
377 377 kill_kernel=False,
378 378 mathjax_url=self.mathjax_url,
379 379 )
380 380 )
381 381
382 382
383 383 #-----------------------------------------------------------------------------
384 384 # Kernel handlers
385 385 #-----------------------------------------------------------------------------
386 386
387 387
388 388 class MainKernelHandler(IPythonHandler):
389 389
390 390 @web.authenticated
391 391 def get(self):
392 392 km = self.kernel_manager
393 393 self.finish(jsonapi.dumps(km.list_kernel_ids()))
394 394
395 395 @web.authenticated
396 396 def post(self):
397 397 km = self.kernel_manager
398 398 nbm = self.notebook_manager
399 399 notebook_id = self.get_argument('notebook', default=None)
400 400 kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
401 401 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
402 402 self.set_header('Location', '/'+kernel_id)
403 403 self.finish(jsonapi.dumps(data))
404 404
405 405
406 406 class KernelHandler(IPythonHandler):
407 407
408 408 SUPPORTED_METHODS = ('DELETE')
409 409
410 410 @web.authenticated
411 411 def delete(self, kernel_id):
412 412 km = self.kernel_manager
413 413 km.shutdown_kernel(kernel_id)
414 414 self.set_status(204)
415 415 self.finish()
416 416
417 417
418 418 class KernelActionHandler(IPythonHandler):
419 419
420 420 @web.authenticated
421 421 def post(self, kernel_id, action):
422 422 km = self.kernel_manager
423 423 if action == 'interrupt':
424 424 km.interrupt_kernel(kernel_id)
425 425 self.set_status(204)
426 426 if action == 'restart':
427 427 km.restart_kernel(kernel_id)
428 428 data = {'ws_url':self.ws_url, 'kernel_id':kernel_id}
429 429 self.set_header('Location', '/'+kernel_id)
430 430 self.write(jsonapi.dumps(data))
431 431 self.finish()
432 432
433 433
434 434 class ZMQStreamHandler(websocket.WebSocketHandler):
435 435
436 436 def clear_cookie(self, *args, **kwargs):
437 437 """meaningless for websockets"""
438 438 pass
439 439
440 440 def _reserialize_reply(self, msg_list):
441 441 """Reserialize a reply message using JSON.
442 442
443 443 This takes the msg list from the ZMQ socket, unserializes it using
444 444 self.session and then serializes the result using JSON. This method
445 445 should be used by self._on_zmq_reply to build messages that can
446 446 be sent back to the browser.
447 447 """
448 448 idents, msg_list = self.session.feed_identities(msg_list)
449 449 msg = self.session.unserialize(msg_list)
450 450 try:
451 451 msg['header'].pop('date')
452 452 except KeyError:
453 453 pass
454 454 try:
455 455 msg['parent_header'].pop('date')
456 456 except KeyError:
457 457 pass
458 458 msg.pop('buffers')
459 459 return jsonapi.dumps(msg, default=date_default)
460 460
461 461 def _on_zmq_reply(self, msg_list):
462 462 # Sometimes this gets triggered when the on_close method is scheduled in the
463 463 # eventloop but hasn't been called.
464 464 if self.stream.closed(): return
465 465 try:
466 466 msg = self._reserialize_reply(msg_list)
467 467 except Exception:
468 468 self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
469 469 else:
470 470 self.write_message(msg)
471 471
472 472 def allow_draft76(self):
473 473 """Allow draft 76, until browsers such as Safari update to RFC 6455.
474 474
475 475 This has been disabled by default in tornado in release 2.2.0, and
476 476 support will be removed in later versions.
477 477 """
478 478 return True
479 479
480 480
481 481 class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
482 482
483 483 def open(self, kernel_id):
484 484 self.kernel_id = kernel_id.decode('ascii')
485 485 self.session = Session(config=self.config)
486 486 self.save_on_message = self.on_message
487 487 self.on_message = self.on_first_message
488 488
489 489 def _inject_cookie_message(self, msg):
490 490 """Inject the first message, which is the document cookie,
491 491 for authentication."""
492 492 if not PY3 and isinstance(msg, unicode):
493 493 # Cookie constructor doesn't accept unicode strings
494 494 # under Python 2.x for some reason
495 495 msg = msg.encode('utf8', 'replace')
496 496 try:
497 497 identity, msg = msg.split(':', 1)
498 498 self.session.session = identity.decode('ascii')
499 499 except Exception:
500 500 logging.error("First ws message didn't have the form 'identity:[cookie]' - %r", msg)
501 501
502 502 try:
503 503 self.request._cookies = Cookie.SimpleCookie(msg)
504 504 except:
505 505 self.log.warn("couldn't parse cookie string: %s",msg, exc_info=True)
506 506
507 507 def on_first_message(self, msg):
508 508 self._inject_cookie_message(msg)
509 509 if self.get_current_user() is None:
510 510 self.log.warn("Couldn't authenticate WebSocket connection")
511 511 raise web.HTTPError(403)
512 512 self.on_message = self.save_on_message
513 513
514 514
515 515 class ZMQChannelHandler(AuthenticatedZMQStreamHandler):
516 516
517 517 @property
518 518 def max_msg_size(self):
519 519 return self.settings.get('max_msg_size', 65535)
520 520
521 521 def create_stream(self):
522 522 km = self.kernel_manager
523 523 meth = getattr(km, 'connect_%s' % self.channel)
524 524 self.zmq_stream = meth(self.kernel_id, identity=self.session.bsession)
525 525
526 526 def initialize(self, *args, **kwargs):
527 527 self.zmq_stream = None
528 528
529 529 def on_first_message(self, msg):
530 530 try:
531 531 super(ZMQChannelHandler, self).on_first_message(msg)
532 532 except web.HTTPError:
533 533 self.close()
534 534 return
535 535 try:
536 536 self.create_stream()
537 537 except web.HTTPError:
538 538 # WebSockets don't response to traditional error codes so we
539 539 # close the connection.
540 540 if not self.stream.closed():
541 541 self.stream.close()
542 542 self.close()
543 543 else:
544 544 self.zmq_stream.on_recv(self._on_zmq_reply)
545 545
546 546 def on_message(self, msg):
547 547 if len(msg) < self.max_msg_size:
548 548 msg = jsonapi.loads(msg)
549 549 self.session.send(self.zmq_stream, msg)
550 550
551 551 def on_close(self):
552 552 # This method can be called twice, once by self.kernel_died and once
553 553 # from the WebSocket close event. If the WebSocket connection is
554 554 # closed before the ZMQ streams are setup, they could be None.
555 555 if self.zmq_stream is not None and not self.zmq_stream.closed():
556 556 self.zmq_stream.on_recv(None)
557 557 self.zmq_stream.close()
558 558
559 559
560 560 class IOPubHandler(ZMQChannelHandler):
561 561 channel = 'iopub'
562 562
563 563 def create_stream(self):
564 564 super(IOPubHandler, self).create_stream()
565 565 km = self.kernel_manager
566 566 km.add_restart_callback(self.kernel_id, self.on_kernel_restarted)
567 567 km.add_restart_callback(self.kernel_id, self.on_restart_failed, 'dead')
568 568
569 569 def on_close(self):
570 570 km = self.kernel_manager
571 571 if self.kernel_id in km:
572 572 km.remove_restart_callback(
573 573 self.kernel_id, self.on_kernel_restarted,
574 574 )
575 575 km.remove_restart_callback(
576 576 self.kernel_id, self.on_restart_failed, 'dead',
577 577 )
578 578 super(IOPubHandler, self).on_close()
579 579
580 580 def _send_status_message(self, status):
581 581 msg = self.session.msg("status",
582 582 {'execution_state': status}
583 583 )
584 584 self.write_message(jsonapi.dumps(msg, default=date_default))
585 585
586 586 def on_kernel_restarted(self):
587 587 logging.warn("kernel %s restarted", self.kernel_id)
588 588 self._send_status_message('restarting')
589 589
590 590 def on_restart_failed(self):
591 591 logging.error("kernel %s restarted failed!", self.kernel_id)
592 592 self._send_status_message('dead')
593 593
594 594 def on_message(self, msg):
595 595 """IOPub messages make no sense"""
596 596 pass
597 597
598 598 class ShellHandler(ZMQChannelHandler):
599 599 channel = 'shell'
600 600
601 601 class StdinHandler(ZMQChannelHandler):
602 602 channel = 'stdin'
603 603
604 604
605 605 #-----------------------------------------------------------------------------
606 606 # Notebook web service handlers
607 607 #-----------------------------------------------------------------------------
608 608
609 609 class NotebookRedirectHandler(IPythonHandler):
610 610
611 611 @authenticate_unless_readonly
612 612 def get(self, notebook_name):
613 613 # strip trailing .ipynb:
614 614 notebook_name = os.path.splitext(notebook_name)[0]
615 615 notebook_id = self.notebook_manager.rev_mapping.get(notebook_name, '')
616 616 if notebook_id:
617 617 url = self.settings.get('base_project_url', '/') + notebook_id
618 618 return self.redirect(url)
619 619 else:
620 620 raise HTTPError(404)
621 621
622 622
623 623 class NotebookRootHandler(IPythonHandler):
624 624
625 625 @authenticate_unless_readonly
626 626 def get(self):
627 627 nbm = self.notebook_manager
628 628 km = self.kernel_manager
629 629 files = nbm.list_notebooks()
630 630 for f in files :
631 631 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
632 632 self.finish(jsonapi.dumps(files))
633 633
634 634 @web.authenticated
635 635 def post(self):
636 636 nbm = self.notebook_manager
637 637 body = self.request.body.strip()
638 638 format = self.get_argument('format', default='json')
639 639 name = self.get_argument('name', default=None)
640 640 if body:
641 641 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
642 642 else:
643 643 notebook_id = nbm.new_notebook()
644 644 self.set_header('Location', '/'+notebook_id)
645 645 self.finish(jsonapi.dumps(notebook_id))
646 646
647 647
648 648 class NotebookHandler(IPythonHandler):
649 649
650 650 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
651 651
652 652 @authenticate_unless_readonly
653 653 def get(self, notebook_id):
654 654 nbm = self.notebook_manager
655 655 format = self.get_argument('format', default='json')
656 656 last_mod, name, data = nbm.get_notebook(notebook_id, format)
657 657
658 658 if format == u'json':
659 659 self.set_header('Content-Type', 'application/json')
660 660 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
661 661 elif format == u'py':
662 662 self.set_header('Content-Type', 'application/x-python')
663 663 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
664 664 self.set_header('Last-Modified', last_mod)
665 665 self.finish(data)
666 666
667 667 @web.authenticated
668 668 def put(self, notebook_id):
669 669 nbm = self.notebook_manager
670 670 format = self.get_argument('format', default='json')
671 671 name = self.get_argument('name', default=None)
672 672 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
673 673 self.set_status(204)
674 674 self.finish()
675 675
676 676 @web.authenticated
677 677 def delete(self, notebook_id):
678 678 self.notebook_manager.delete_notebook(notebook_id)
679 679 self.set_status(204)
680 680 self.finish()
681 681
682 682
683 class NotebookCheckpointsHandler(AuthenticatedHandler):
683 class NotebookCheckpointsHandler(IPythonHandler):
684 684
685 685 SUPPORTED_METHODS = ('GET', 'POST')
686 686
687 687 @web.authenticated
688 688 def get(self, notebook_id):
689 689 """get lists checkpoints for a notebook"""
690 nbm = self.application.notebook_manager
690 nbm = self.notebook_manager
691 691 checkpoints = nbm.list_checkpoints(notebook_id)
692 692 data = jsonapi.dumps(checkpoints, default=date_default)
693 693 self.finish(data)
694 694
695 695 @web.authenticated
696 696 def post(self, notebook_id):
697 697 """post creates a new checkpoint"""
698 nbm = self.application.notebook_manager
698 nbm = self.notebook_manager
699 699 checkpoint = nbm.create_checkpoint(notebook_id)
700 700 data = jsonapi.dumps(checkpoint, default=date_default)
701
701 702 self.finish(data)
702 703
703 704
704 class ModifyNotebookCheckpointsHandler(AuthenticatedHandler):
705 class ModifyNotebookCheckpointsHandler(IPythonHandler):
705 706
706 707 SUPPORTED_METHODS = ('POST', 'DELETE')
707 708
708 709 @web.authenticated
709 710 def post(self, notebook_id, checkpoint_id):
710 711 """post restores a notebook from a checkpoint"""
711 nbm = self.application.notebook_manager
712 nbm = self.notebook_manager
712 713 nbm.restore_checkpoint(notebook_id, checkpoint_id)
713 714 self.set_status(204)
714 715 self.finish()
715 716
716 717 @web.authenticated
717 718 def delete(self, notebook_id, checkpoint_id):
718 719 """delete clears a checkpoint for a given notebook"""
719 nbm = self.application.notebook_manager
720 nbm = self.notebook_manager
720 721 nbm.delte_checkpoint(notebook_id, checkpoint_id)
721 722 self.set_status(204)
722 723 self.finish()
723 724
724 725
725
726 726 class NotebookCopyHandler(IPythonHandler):
727 727
728 728 @web.authenticated
729 729 def get(self, notebook_id):
730 730 notebook_id = self.notebook_manager.copy_notebook(notebook_id)
731 731 self.redirect('/'+urljoin(self.base_project_url, notebook_id))
732 732
733 733
734 734 #-----------------------------------------------------------------------------
735 735 # Cluster handlers
736 736 #-----------------------------------------------------------------------------
737 737
738 738
739 739 class MainClusterHandler(IPythonHandler):
740 740
741 741 @web.authenticated
742 742 def get(self):
743 743 self.finish(jsonapi.dumps(self.cluster_manager.list_profiles()))
744 744
745 745
746 746 class ClusterProfileHandler(IPythonHandler):
747 747
748 748 @web.authenticated
749 749 def get(self, profile):
750 750 self.finish(jsonapi.dumps(self.cluster_manager.profile_info(profile)))
751 751
752 752
753 753 class ClusterActionHandler(IPythonHandler):
754 754
755 755 @web.authenticated
756 756 def post(self, profile, action):
757 757 cm = self.cluster_manager
758 758 if action == 'start':
759 759 n = self.get_argument('n',default=None)
760 760 if n is None:
761 761 data = cm.start_cluster(profile)
762 762 else:
763 763 data = cm.start_cluster(profile, int(n))
764 764 if action == 'stop':
765 765 data = cm.stop_cluster(profile)
766 766 self.finish(jsonapi.dumps(data))
767 767
768 768
769 769 #-----------------------------------------------------------------------------
770 770 # File handler
771 771 #-----------------------------------------------------------------------------
772 772
773 773 # to minimize subclass changes:
774 774 HTTPError = web.HTTPError
775 775
776 776 class FileFindHandler(web.StaticFileHandler):
777 777 """subclass of StaticFileHandler for serving files from a search path"""
778 778
779 779 _static_paths = {}
780 780 # _lock is needed for tornado < 2.2.0 compat
781 781 _lock = threading.Lock() # protects _static_hashes
782 782
783 783 def initialize(self, path, default_filename=None):
784 784 if isinstance(path, basestring):
785 785 path = [path]
786 786 self.roots = tuple(
787 787 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
788 788 )
789 789 self.default_filename = default_filename
790 790
791 791 @classmethod
792 792 def locate_file(cls, path, roots):
793 793 """locate a file to serve on our static file search path"""
794 794 with cls._lock:
795 795 if path in cls._static_paths:
796 796 return cls._static_paths[path]
797 797 try:
798 798 abspath = os.path.abspath(filefind(path, roots))
799 799 except IOError:
800 800 # empty string should always give exists=False
801 801 return ''
802 802
803 803 # os.path.abspath strips a trailing /
804 804 # it needs to be temporarily added back for requests to root/
805 805 if not (abspath + os.path.sep).startswith(roots):
806 806 raise HTTPError(403, "%s is not in root static directory", path)
807 807
808 808 cls._static_paths[path] = abspath
809 809 return abspath
810 810
811 811 def get(self, path, include_body=True):
812 812 path = self.parse_url_path(path)
813 813
814 814 # begin subclass override
815 815 abspath = self.locate_file(path, self.roots)
816 816 # end subclass override
817 817
818 818 if os.path.isdir(abspath) and self.default_filename is not None:
819 819 # need to look at the request.path here for when path is empty
820 820 # but there is some prefix to the path that was already
821 821 # trimmed by the routing
822 822 if not self.request.path.endswith("/"):
823 823 self.redirect(self.request.path + "/")
824 824 return
825 825 abspath = os.path.join(abspath, self.default_filename)
826 826 if not os.path.exists(abspath):
827 827 raise HTTPError(404)
828 828 if not os.path.isfile(abspath):
829 829 raise HTTPError(403, "%s is not a file", path)
830 830
831 831 stat_result = os.stat(abspath)
832 832 modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
833 833
834 834 self.set_header("Last-Modified", modified)
835 835
836 836 mime_type, encoding = mimetypes.guess_type(abspath)
837 837 if mime_type:
838 838 self.set_header("Content-Type", mime_type)
839 839
840 840 cache_time = self.get_cache_time(path, modified, mime_type)
841 841
842 842 if cache_time > 0:
843 843 self.set_header("Expires", datetime.datetime.utcnow() + \
844 844 datetime.timedelta(seconds=cache_time))
845 845 self.set_header("Cache-Control", "max-age=" + str(cache_time))
846 846 else:
847 847 self.set_header("Cache-Control", "public")
848 848
849 849 self.set_extra_headers(path)
850 850
851 851 # Check the If-Modified-Since, and don't send the result if the
852 852 # content has not been modified
853 853 ims_value = self.request.headers.get("If-Modified-Since")
854 854 if ims_value is not None:
855 855 date_tuple = email.utils.parsedate(ims_value)
856 856 if_since = datetime.datetime(*date_tuple[:6])
857 857 if if_since >= modified:
858 858 self.set_status(304)
859 859 return
860 860
861 861 with open(abspath, "rb") as file:
862 862 data = file.read()
863 863 hasher = hashlib.sha1()
864 864 hasher.update(data)
865 865 self.set_header("Etag", '"%s"' % hasher.hexdigest())
866 866 if include_body:
867 867 self.write(data)
868 868 else:
869 869 assert self.request.method == "HEAD"
870 870 self.set_header("Content-Length", len(data))
871 871
872 872 @classmethod
873 873 def get_version(cls, settings, path):
874 874 """Generate the version string to be used in static URLs.
875 875
876 876 This method may be overridden in subclasses (but note that it
877 877 is a class method rather than a static method). The default
878 878 implementation uses a hash of the file's contents.
879 879
880 880 ``settings`` is the `Application.settings` dictionary and ``path``
881 881 is the relative location of the requested asset on the filesystem.
882 882 The returned value should be a string, or ``None`` if no version
883 883 could be determined.
884 884 """
885 885 # begin subclass override:
886 886 static_paths = settings['static_path']
887 887 if isinstance(static_paths, basestring):
888 888 static_paths = [static_paths]
889 889 roots = tuple(
890 890 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
891 891 )
892 892
893 893 try:
894 894 abs_path = filefind(path, roots)
895 895 except IOError:
896 896 app_log.error("Could not find static file %r", path)
897 897 return None
898 898
899 899 # end subclass override
900 900
901 901 with cls._lock:
902 902 hashes = cls._static_hashes
903 903 if abs_path not in hashes:
904 904 try:
905 905 f = open(abs_path, "rb")
906 906 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
907 907 f.close()
908 908 except Exception:
909 909 app_log.error("Could not open static file %r", path)
910 910 hashes[abs_path] = None
911 911 hsh = hashes.get(abs_path)
912 912 if hsh:
913 913 return hsh[:5]
914 914 return None
915 915
916 916
917 917 def parse_url_path(self, url_path):
918 918 """Converts a static URL path into a filesystem path.
919 919
920 920 ``url_path`` is the path component of the URL with
921 921 ``static_url_prefix`` removed. The return value should be
922 922 filesystem path relative to ``static_path``.
923 923 """
924 924 if os.path.sep != "/":
925 925 url_path = url_path.replace("/", os.path.sep)
926 926 return url_path
927 927
928 928
General Comments 0
You need to be logged in to leave comments. Login now