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