##// END OF EJS Templates
Removing print handler.
Brian E. Granger -
Show More
@@ -1,898 +1,885
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 class PrintNotebookHandler(IPythonHandler):
384
385 @authenticate_unless_readonly
386 def get(self, notebook_id):
387 if not self.notebook_manager.notebook_exists(notebook_id):
388 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
389 self.write( self.render_template('printnotebook.html',
390 project=self.project,
391 notebook_id=notebook_id,
392 kill_kernel=False,
393 mathjax_url=self.mathjax_url,
394 ))
395
396 383 #-----------------------------------------------------------------------------
397 384 # Kernel handlers
398 385 #-----------------------------------------------------------------------------
399 386
400 387
401 388 class MainKernelHandler(IPythonHandler):
402 389
403 390 @web.authenticated
404 391 def get(self):
405 392 km = self.kernel_manager
406 393 self.finish(jsonapi.dumps(km.list_kernel_ids()))
407 394
408 395 @web.authenticated
409 396 def post(self):
410 397 km = self.kernel_manager
411 398 nbm = self.notebook_manager
412 399 notebook_id = self.get_argument('notebook', default=None)
413 400 kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
414 401 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
415 402 self.set_header('Location', '/'+kernel_id)
416 403 self.finish(jsonapi.dumps(data))
417 404
418 405
419 406 class KernelHandler(IPythonHandler):
420 407
421 408 SUPPORTED_METHODS = ('DELETE')
422 409
423 410 @web.authenticated
424 411 def delete(self, kernel_id):
425 412 km = self.kernel_manager
426 413 km.shutdown_kernel(kernel_id)
427 414 self.set_status(204)
428 415 self.finish()
429 416
430 417
431 418 class KernelActionHandler(IPythonHandler):
432 419
433 420 @web.authenticated
434 421 def post(self, kernel_id, action):
435 422 km = self.kernel_manager
436 423 if action == 'interrupt':
437 424 km.interrupt_kernel(kernel_id)
438 425 self.set_status(204)
439 426 if action == 'restart':
440 427 km.restart_kernel(kernel_id)
441 428 data = {'ws_url':self.ws_url, 'kernel_id':kernel_id}
442 429 self.set_header('Location', '/'+kernel_id)
443 430 self.write(jsonapi.dumps(data))
444 431 self.finish()
445 432
446 433
447 434 class ZMQStreamHandler(websocket.WebSocketHandler):
448 435
449 436 def clear_cookie(self, *args, **kwargs):
450 437 """meaningless for websockets"""
451 438 pass
452 439
453 440 def _reserialize_reply(self, msg_list):
454 441 """Reserialize a reply message using JSON.
455 442
456 443 This takes the msg list from the ZMQ socket, unserializes it using
457 444 self.session and then serializes the result using JSON. This method
458 445 should be used by self._on_zmq_reply to build messages that can
459 446 be sent back to the browser.
460 447 """
461 448 idents, msg_list = self.session.feed_identities(msg_list)
462 449 msg = self.session.unserialize(msg_list)
463 450 try:
464 451 msg['header'].pop('date')
465 452 except KeyError:
466 453 pass
467 454 try:
468 455 msg['parent_header'].pop('date')
469 456 except KeyError:
470 457 pass
471 458 msg.pop('buffers')
472 459 return jsonapi.dumps(msg, default=date_default)
473 460
474 461 def _on_zmq_reply(self, msg_list):
475 462 # Sometimes this gets triggered when the on_close method is scheduled in the
476 463 # eventloop but hasn't been called.
477 464 if self.stream.closed(): return
478 465 try:
479 466 msg = self._reserialize_reply(msg_list)
480 467 except Exception:
481 468 self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
482 469 else:
483 470 self.write_message(msg)
484 471
485 472 def allow_draft76(self):
486 473 """Allow draft 76, until browsers such as Safari update to RFC 6455.
487 474
488 475 This has been disabled by default in tornado in release 2.2.0, and
489 476 support will be removed in later versions.
490 477 """
491 478 return True
492 479
493 480
494 481 class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
495 482
496 483 def open(self, kernel_id):
497 484 self.kernel_id = kernel_id.decode('ascii')
498 485 self.session = Session(config=self.config)
499 486 self.save_on_message = self.on_message
500 487 self.on_message = self.on_first_message
501 488
502 489 def _inject_cookie_message(self, msg):
503 490 """Inject the first message, which is the document cookie,
504 491 for authentication."""
505 492 if not PY3 and isinstance(msg, unicode):
506 493 # Cookie constructor doesn't accept unicode strings
507 494 # under Python 2.x for some reason
508 495 msg = msg.encode('utf8', 'replace')
509 496 try:
510 497 identity, msg = msg.split(':', 1)
511 498 self.session.session = identity.decode('ascii')
512 499 except Exception:
513 500 logging.error("First ws message didn't have the form 'identity:[cookie]' - %r", msg)
514 501
515 502 try:
516 503 self.request._cookies = Cookie.SimpleCookie(msg)
517 504 except:
518 505 self.log.warn("couldn't parse cookie string: %s",msg, exc_info=True)
519 506
520 507 def on_first_message(self, msg):
521 508 self._inject_cookie_message(msg)
522 509 if self.get_current_user() is None:
523 510 self.log.warn("Couldn't authenticate WebSocket connection")
524 511 raise web.HTTPError(403)
525 512 self.on_message = self.save_on_message
526 513
527 514
528 515 class ZMQChannelHandler(AuthenticatedZMQStreamHandler):
529 516
530 517 @property
531 518 def max_msg_size(self):
532 519 return self.settings.get('max_msg_size', 65535)
533 520
534 521 def create_stream(self):
535 522 km = self.kernel_manager
536 523 meth = getattr(km, 'connect_%s' % self.channel)
537 524 self.zmq_stream = meth(self.kernel_id, identity=self.session.bsession)
538 525
539 526 def initialize(self, *args, **kwargs):
540 527 self.zmq_stream = None
541 528
542 529 def on_first_message(self, msg):
543 530 try:
544 531 super(ZMQChannelHandler, self).on_first_message(msg)
545 532 except web.HTTPError:
546 533 self.close()
547 534 return
548 535 try:
549 536 self.create_stream()
550 537 except web.HTTPError:
551 538 # WebSockets don't response to traditional error codes so we
552 539 # close the connection.
553 540 if not self.stream.closed():
554 541 self.stream.close()
555 542 self.close()
556 543 else:
557 544 self.zmq_stream.on_recv(self._on_zmq_reply)
558 545
559 546 def on_message(self, msg):
560 547 if len(msg) < self.max_msg_size:
561 548 msg = jsonapi.loads(msg)
562 549 self.session.send(self.zmq_stream, msg)
563 550
564 551 def on_close(self):
565 552 # This method can be called twice, once by self.kernel_died and once
566 553 # from the WebSocket close event. If the WebSocket connection is
567 554 # closed before the ZMQ streams are setup, they could be None.
568 555 if self.zmq_stream is not None and not self.zmq_stream.closed():
569 556 self.zmq_stream.on_recv(None)
570 557 self.zmq_stream.close()
571 558
572 559
573 560 class IOPubHandler(ZMQChannelHandler):
574 561 channel = 'iopub'
575 562
576 563 def create_stream(self):
577 564 super(IOPubHandler, self).create_stream()
578 565 km = self.kernel_manager
579 566 km.add_restart_callback(self.kernel_id, self.on_kernel_restarted)
580 567 km.add_restart_callback(self.kernel_id, self.on_restart_failed, 'dead')
581 568
582 569 def on_close(self):
583 570 km = self.kernel_manager
584 571 if self.kernel_id in km:
585 572 km.remove_restart_callback(
586 573 self.kernel_id, self.on_kernel_restarted,
587 574 )
588 575 km.remove_restart_callback(
589 576 self.kernel_id, self.on_restart_failed, 'dead',
590 577 )
591 578 super(IOPubHandler, self).on_close()
592 579
593 580 def _send_status_message(self, status):
594 581 msg = self.session.msg("status",
595 582 {'execution_state': status}
596 583 )
597 584 self.write_message(jsonapi.dumps(msg, default=date_default))
598 585
599 586 def on_kernel_restarted(self):
600 587 logging.warn("kernel %s restarted", self.kernel_id)
601 588 self._send_status_message('restarting')
602 589
603 590 def on_restart_failed(self):
604 591 logging.error("kernel %s restarted failed!", self.kernel_id)
605 592 self._send_status_message('dead')
606 593
607 594 def on_message(self, msg):
608 595 """IOPub messages make no sense"""
609 596 pass
610 597
611 598 class ShellHandler(ZMQChannelHandler):
612 599 channel = 'shell'
613 600
614 601 class StdinHandler(ZMQChannelHandler):
615 602 channel = 'stdin'
616 603
617 604
618 605 #-----------------------------------------------------------------------------
619 606 # Notebook web service handlers
620 607 #-----------------------------------------------------------------------------
621 608
622 609 class NotebookRedirectHandler(IPythonHandler):
623 610
624 611 @authenticate_unless_readonly
625 612 def get(self, notebook_name):
626 613 # strip trailing .ipynb:
627 614 notebook_name = os.path.splitext(notebook_name)[0]
628 615 notebook_id = self.notebook_manager.rev_mapping.get(notebook_name, '')
629 616 if notebook_id:
630 617 url = self.settings.get('base_project_url', '/') + notebook_id
631 618 return self.redirect(url)
632 619 else:
633 620 raise HTTPError(404)
634 621
635 622
636 623 class NotebookRootHandler(IPythonHandler):
637 624
638 625 @authenticate_unless_readonly
639 626 def get(self):
640 627 nbm = self.notebook_manager
641 628 km = self.kernel_manager
642 629 files = nbm.list_notebooks()
643 630 for f in files :
644 631 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
645 632 self.finish(jsonapi.dumps(files))
646 633
647 634 @web.authenticated
648 635 def post(self):
649 636 nbm = self.notebook_manager
650 637 body = self.request.body.strip()
651 638 format = self.get_argument('format', default='json')
652 639 name = self.get_argument('name', default=None)
653 640 if body:
654 641 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
655 642 else:
656 643 notebook_id = nbm.new_notebook()
657 644 self.set_header('Location', '/'+notebook_id)
658 645 self.finish(jsonapi.dumps(notebook_id))
659 646
660 647
661 648 class NotebookHandler(IPythonHandler):
662 649
663 650 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
664 651
665 652 @authenticate_unless_readonly
666 653 def get(self, notebook_id):
667 654 nbm = self.notebook_manager
668 655 format = self.get_argument('format', default='json')
669 656 last_mod, name, data = nbm.get_notebook(notebook_id, format)
670 657
671 658 if format == u'json':
672 659 self.set_header('Content-Type', 'application/json')
673 660 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
674 661 elif format == u'py':
675 662 self.set_header('Content-Type', 'application/x-python')
676 663 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
677 664 self.set_header('Last-Modified', last_mod)
678 665 self.finish(data)
679 666
680 667 @web.authenticated
681 668 def put(self, notebook_id):
682 669 nbm = self.notebook_manager
683 670 format = self.get_argument('format', default='json')
684 671 name = self.get_argument('name', default=None)
685 672 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
686 673 self.set_status(204)
687 674 self.finish()
688 675
689 676 @web.authenticated
690 677 def delete(self, notebook_id):
691 678 self.notebook_manager.delete_notebook(notebook_id)
692 679 self.set_status(204)
693 680 self.finish()
694 681
695 682
696 683 class NotebookCopyHandler(IPythonHandler):
697 684
698 685 @web.authenticated
699 686 def get(self, notebook_id):
700 687 notebook_id = self.notebook_manager.copy_notebook(notebook_id)
701 688 self.redirect('/'+urljoin(self.base_project_url, notebook_id))
702 689
703 690
704 691 #-----------------------------------------------------------------------------
705 692 # Cluster handlers
706 693 #-----------------------------------------------------------------------------
707 694
708 695
709 696 class MainClusterHandler(IPythonHandler):
710 697
711 698 @web.authenticated
712 699 def get(self):
713 700 self.finish(jsonapi.dumps(self.cluster_manager.list_profiles()))
714 701
715 702
716 703 class ClusterProfileHandler(IPythonHandler):
717 704
718 705 @web.authenticated
719 706 def get(self, profile):
720 707 self.finish(jsonapi.dumps(self.cluster_manager.profile_info(profile)))
721 708
722 709
723 710 class ClusterActionHandler(IPythonHandler):
724 711
725 712 @web.authenticated
726 713 def post(self, profile, action):
727 714 cm = self.cluster_manager
728 715 if action == 'start':
729 716 n = self.get_argument('n',default=None)
730 717 if n is None:
731 718 data = cm.start_cluster(profile)
732 719 else:
733 720 data = cm.start_cluster(profile, int(n))
734 721 if action == 'stop':
735 722 data = cm.stop_cluster(profile)
736 723 self.finish(jsonapi.dumps(data))
737 724
738 725
739 726 #-----------------------------------------------------------------------------
740 727 # File handler
741 728 #-----------------------------------------------------------------------------
742 729
743 730 # to minimize subclass changes:
744 731 HTTPError = web.HTTPError
745 732
746 733 class FileFindHandler(web.StaticFileHandler):
747 734 """subclass of StaticFileHandler for serving files from a search path"""
748 735
749 736 _static_paths = {}
750 737 # _lock is needed for tornado < 2.2.0 compat
751 738 _lock = threading.Lock() # protects _static_hashes
752 739
753 740 def initialize(self, path, default_filename=None):
754 741 if isinstance(path, basestring):
755 742 path = [path]
756 743 self.roots = tuple(
757 744 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
758 745 )
759 746 self.default_filename = default_filename
760 747
761 748 @classmethod
762 749 def locate_file(cls, path, roots):
763 750 """locate a file to serve on our static file search path"""
764 751 with cls._lock:
765 752 if path in cls._static_paths:
766 753 return cls._static_paths[path]
767 754 try:
768 755 abspath = os.path.abspath(filefind(path, roots))
769 756 except IOError:
770 757 # empty string should always give exists=False
771 758 return ''
772 759
773 760 # os.path.abspath strips a trailing /
774 761 # it needs to be temporarily added back for requests to root/
775 762 if not (abspath + os.path.sep).startswith(roots):
776 763 raise HTTPError(403, "%s is not in root static directory", path)
777 764
778 765 cls._static_paths[path] = abspath
779 766 return abspath
780 767
781 768 def get(self, path, include_body=True):
782 769 path = self.parse_url_path(path)
783 770
784 771 # begin subclass override
785 772 abspath = self.locate_file(path, self.roots)
786 773 # end subclass override
787 774
788 775 if os.path.isdir(abspath) and self.default_filename is not None:
789 776 # need to look at the request.path here for when path is empty
790 777 # but there is some prefix to the path that was already
791 778 # trimmed by the routing
792 779 if not self.request.path.endswith("/"):
793 780 self.redirect(self.request.path + "/")
794 781 return
795 782 abspath = os.path.join(abspath, self.default_filename)
796 783 if not os.path.exists(abspath):
797 784 raise HTTPError(404)
798 785 if not os.path.isfile(abspath):
799 786 raise HTTPError(403, "%s is not a file", path)
800 787
801 788 stat_result = os.stat(abspath)
802 789 modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
803 790
804 791 self.set_header("Last-Modified", modified)
805 792
806 793 mime_type, encoding = mimetypes.guess_type(abspath)
807 794 if mime_type:
808 795 self.set_header("Content-Type", mime_type)
809 796
810 797 cache_time = self.get_cache_time(path, modified, mime_type)
811 798
812 799 if cache_time > 0:
813 800 self.set_header("Expires", datetime.datetime.utcnow() + \
814 801 datetime.timedelta(seconds=cache_time))
815 802 self.set_header("Cache-Control", "max-age=" + str(cache_time))
816 803 else:
817 804 self.set_header("Cache-Control", "public")
818 805
819 806 self.set_extra_headers(path)
820 807
821 808 # Check the If-Modified-Since, and don't send the result if the
822 809 # content has not been modified
823 810 ims_value = self.request.headers.get("If-Modified-Since")
824 811 if ims_value is not None:
825 812 date_tuple = email.utils.parsedate(ims_value)
826 813 if_since = datetime.datetime(*date_tuple[:6])
827 814 if if_since >= modified:
828 815 self.set_status(304)
829 816 return
830 817
831 818 with open(abspath, "rb") as file:
832 819 data = file.read()
833 820 hasher = hashlib.sha1()
834 821 hasher.update(data)
835 822 self.set_header("Etag", '"%s"' % hasher.hexdigest())
836 823 if include_body:
837 824 self.write(data)
838 825 else:
839 826 assert self.request.method == "HEAD"
840 827 self.set_header("Content-Length", len(data))
841 828
842 829 @classmethod
843 830 def get_version(cls, settings, path):
844 831 """Generate the version string to be used in static URLs.
845 832
846 833 This method may be overridden in subclasses (but note that it
847 834 is a class method rather than a static method). The default
848 835 implementation uses a hash of the file's contents.
849 836
850 837 ``settings`` is the `Application.settings` dictionary and ``path``
851 838 is the relative location of the requested asset on the filesystem.
852 839 The returned value should be a string, or ``None`` if no version
853 840 could be determined.
854 841 """
855 842 # begin subclass override:
856 843 static_paths = settings['static_path']
857 844 if isinstance(static_paths, basestring):
858 845 static_paths = [static_paths]
859 846 roots = tuple(
860 847 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
861 848 )
862 849
863 850 try:
864 851 abs_path = filefind(path, roots)
865 852 except IOError:
866 853 app_log.error("Could not find static file %r", path)
867 854 return None
868 855
869 856 # end subclass override
870 857
871 858 with cls._lock:
872 859 hashes = cls._static_hashes
873 860 if abs_path not in hashes:
874 861 try:
875 862 f = open(abs_path, "rb")
876 863 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
877 864 f.close()
878 865 except Exception:
879 866 app_log.error("Could not open static file %r", path)
880 867 hashes[abs_path] = None
881 868 hsh = hashes.get(abs_path)
882 869 if hsh:
883 870 return hsh[:5]
884 871 return None
885 872
886 873
887 874 def parse_url_path(self, url_path):
888 875 """Converts a static URL path into a filesystem path.
889 876
890 877 ``url_path`` is the path component of the URL with
891 878 ``static_url_prefix`` removed. The return value should be
892 879 filesystem path relative to ``static_path``.
893 880 """
894 881 if os.path.sep != "/":
895 882 url_path = url_path.replace("/", os.path.sep)
896 883 return url_path
897 884
898 885
General Comments 0
You need to be logged in to leave comments. Login now