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