##// END OF EJS Templates
Merge pull request #6654 from Carreau/closes-6647...
Min RK -
r18293:4515e355 merge
parent child Browse files
Show More
@@ -1,460 +1,470 b''
1 """Base Tornado handlers for the notebook server."""
1 """Base Tornado handlers for the notebook server."""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import functools
6 import functools
7 import json
7 import json
8 import logging
8 import logging
9 import os
9 import os
10 import re
10 import re
11 import sys
11 import sys
12 import traceback
12 import traceback
13 try:
13 try:
14 # py3
14 # py3
15 from http.client import responses
15 from http.client import responses
16 except ImportError:
16 except ImportError:
17 from httplib import responses
17 from httplib import responses
18
18
19 from jinja2 import TemplateNotFound
19 from jinja2 import TemplateNotFound
20 from tornado import web
20 from tornado import web
21
21
22 try:
22 try:
23 from tornado.log import app_log
23 from tornado.log import app_log
24 except ImportError:
24 except ImportError:
25 app_log = logging.getLogger()
25 app_log = logging.getLogger()
26
26
27 import IPython
28
27 from IPython.config import Application
29 from IPython.config import Application
28 from IPython.utils.path import filefind
30 from IPython.utils.path import filefind
29 from IPython.utils.py3compat import string_types
31 from IPython.utils.py3compat import string_types
30 from IPython.html.utils import is_hidden, url_path_join, url_escape
32 from IPython.html.utils import is_hidden, url_path_join, url_escape
31
33
32 #-----------------------------------------------------------------------------
34 #-----------------------------------------------------------------------------
33 # Top-level handlers
35 # Top-level handlers
34 #-----------------------------------------------------------------------------
36 #-----------------------------------------------------------------------------
35 non_alphanum = re.compile(r'[^A-Za-z0-9]')
37 non_alphanum = re.compile(r'[^A-Za-z0-9]')
36
38
37 class AuthenticatedHandler(web.RequestHandler):
39 class AuthenticatedHandler(web.RequestHandler):
38 """A RequestHandler with an authenticated user."""
40 """A RequestHandler with an authenticated user."""
39
41
40 def set_default_headers(self):
42 def set_default_headers(self):
41 headers = self.settings.get('headers', {})
43 headers = self.settings.get('headers', {})
42
44
43 if "X-Frame-Options" not in headers:
45 if "X-Frame-Options" not in headers:
44 headers["X-Frame-Options"] = "SAMEORIGIN"
46 headers["X-Frame-Options"] = "SAMEORIGIN"
45
47
46 for header_name,value in headers.items() :
48 for header_name,value in headers.items() :
47 try:
49 try:
48 self.set_header(header_name, value)
50 self.set_header(header_name, value)
49 except Exception:
51 except Exception:
50 # tornado raise Exception (not a subclass)
52 # tornado raise Exception (not a subclass)
51 # if method is unsupported (websocket and Access-Control-Allow-Origin
53 # if method is unsupported (websocket and Access-Control-Allow-Origin
52 # for example, so just ignore)
54 # for example, so just ignore)
53 pass
55 pass
54
56
55 def clear_login_cookie(self):
57 def clear_login_cookie(self):
56 self.clear_cookie(self.cookie_name)
58 self.clear_cookie(self.cookie_name)
57
59
58 def get_current_user(self):
60 def get_current_user(self):
59 user_id = self.get_secure_cookie(self.cookie_name)
61 user_id = self.get_secure_cookie(self.cookie_name)
60 # For now the user_id should not return empty, but it could eventually
62 # For now the user_id should not return empty, but it could eventually
61 if user_id == '':
63 if user_id == '':
62 user_id = 'anonymous'
64 user_id = 'anonymous'
63 if user_id is None:
65 if user_id is None:
64 # prevent extra Invalid cookie sig warnings:
66 # prevent extra Invalid cookie sig warnings:
65 self.clear_login_cookie()
67 self.clear_login_cookie()
66 if not self.login_available:
68 if not self.login_available:
67 user_id = 'anonymous'
69 user_id = 'anonymous'
68 return user_id
70 return user_id
69
71
70 @property
72 @property
71 def cookie_name(self):
73 def cookie_name(self):
72 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
74 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
73 self.request.host
75 self.request.host
74 ))
76 ))
75 return self.settings.get('cookie_name', default_cookie_name)
77 return self.settings.get('cookie_name', default_cookie_name)
76
78
77 @property
79 @property
78 def password(self):
80 def password(self):
79 """our password"""
81 """our password"""
80 return self.settings.get('password', '')
82 return self.settings.get('password', '')
81
83
82 @property
84 @property
83 def logged_in(self):
85 def logged_in(self):
84 """Is a user currently logged in?
86 """Is a user currently logged in?
85
87
86 """
88 """
87 user = self.get_current_user()
89 user = self.get_current_user()
88 return (user and not user == 'anonymous')
90 return (user and not user == 'anonymous')
89
91
90 @property
92 @property
91 def login_available(self):
93 def login_available(self):
92 """May a user proceed to log in?
94 """May a user proceed to log in?
93
95
94 This returns True if login capability is available, irrespective of
96 This returns True if login capability is available, irrespective of
95 whether the user is already logged in or not.
97 whether the user is already logged in or not.
96
98
97 """
99 """
98 return bool(self.settings.get('password', ''))
100 return bool(self.settings.get('password', ''))
99
101
100
102
101 class IPythonHandler(AuthenticatedHandler):
103 class IPythonHandler(AuthenticatedHandler):
102 """IPython-specific extensions to authenticated handling
104 """IPython-specific extensions to authenticated handling
103
105
104 Mostly property shortcuts to IPython-specific settings.
106 Mostly property shortcuts to IPython-specific settings.
105 """
107 """
106
108
107 @property
109 @property
108 def config(self):
110 def config(self):
109 return self.settings.get('config', None)
111 return self.settings.get('config', None)
110
112
111 @property
113 @property
112 def log(self):
114 def log(self):
113 """use the IPython log by default, falling back on tornado's logger"""
115 """use the IPython log by default, falling back on tornado's logger"""
114 if Application.initialized():
116 if Application.initialized():
115 return Application.instance().log
117 return Application.instance().log
116 else:
118 else:
117 return app_log
119 return app_log
118
120
119 #---------------------------------------------------------------
121 #---------------------------------------------------------------
120 # URLs
122 # URLs
121 #---------------------------------------------------------------
123 #---------------------------------------------------------------
122
124
123 @property
125 @property
124 def mathjax_url(self):
126 def mathjax_url(self):
125 return self.settings.get('mathjax_url', '')
127 return self.settings.get('mathjax_url', '')
126
128
127 @property
129 @property
128 def base_url(self):
130 def base_url(self):
129 return self.settings.get('base_url', '/')
131 return self.settings.get('base_url', '/')
130
132
131 @property
133 @property
132 def ws_url(self):
134 def ws_url(self):
133 return self.settings.get('websocket_url', '')
135 return self.settings.get('websocket_url', '')
134
136
135 #---------------------------------------------------------------
137 #---------------------------------------------------------------
136 # Manager objects
138 # Manager objects
137 #---------------------------------------------------------------
139 #---------------------------------------------------------------
138
140
139 @property
141 @property
140 def kernel_manager(self):
142 def kernel_manager(self):
141 return self.settings['kernel_manager']
143 return self.settings['kernel_manager']
142
144
143 @property
145 @property
144 def contents_manager(self):
146 def contents_manager(self):
145 return self.settings['contents_manager']
147 return self.settings['contents_manager']
146
148
147 @property
149 @property
148 def cluster_manager(self):
150 def cluster_manager(self):
149 return self.settings['cluster_manager']
151 return self.settings['cluster_manager']
150
152
151 @property
153 @property
152 def session_manager(self):
154 def session_manager(self):
153 return self.settings['session_manager']
155 return self.settings['session_manager']
154
156
155 @property
157 @property
156 def kernel_spec_manager(self):
158 def kernel_spec_manager(self):
157 return self.settings['kernel_spec_manager']
159 return self.settings['kernel_spec_manager']
158
160
159 #---------------------------------------------------------------
161 #---------------------------------------------------------------
160 # CORS
162 # CORS
161 #---------------------------------------------------------------
163 #---------------------------------------------------------------
162
164
163 @property
165 @property
164 def allow_origin(self):
166 def allow_origin(self):
165 """Normal Access-Control-Allow-Origin"""
167 """Normal Access-Control-Allow-Origin"""
166 return self.settings.get('allow_origin', '')
168 return self.settings.get('allow_origin', '')
167
169
168 @property
170 @property
169 def allow_origin_pat(self):
171 def allow_origin_pat(self):
170 """Regular expression version of allow_origin"""
172 """Regular expression version of allow_origin"""
171 return self.settings.get('allow_origin_pat', None)
173 return self.settings.get('allow_origin_pat', None)
172
174
173 @property
175 @property
174 def allow_credentials(self):
176 def allow_credentials(self):
175 """Whether to set Access-Control-Allow-Credentials"""
177 """Whether to set Access-Control-Allow-Credentials"""
176 return self.settings.get('allow_credentials', False)
178 return self.settings.get('allow_credentials', False)
177
179
178 def set_default_headers(self):
180 def set_default_headers(self):
179 """Add CORS headers, if defined"""
181 """Add CORS headers, if defined"""
180 super(IPythonHandler, self).set_default_headers()
182 super(IPythonHandler, self).set_default_headers()
181 if self.allow_origin:
183 if self.allow_origin:
182 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
184 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
183 elif self.allow_origin_pat:
185 elif self.allow_origin_pat:
184 origin = self.get_origin()
186 origin = self.get_origin()
185 if origin and self.allow_origin_pat.match(origin):
187 if origin and self.allow_origin_pat.match(origin):
186 self.set_header("Access-Control-Allow-Origin", origin)
188 self.set_header("Access-Control-Allow-Origin", origin)
187 if self.allow_credentials:
189 if self.allow_credentials:
188 self.set_header("Access-Control-Allow-Credentials", 'true')
190 self.set_header("Access-Control-Allow-Credentials", 'true')
189
191
190 def get_origin(self):
192 def get_origin(self):
191 # Handle WebSocket Origin naming convention differences
193 # Handle WebSocket Origin naming convention differences
192 # The difference between version 8 and 13 is that in 8 the
194 # The difference between version 8 and 13 is that in 8 the
193 # client sends a "Sec-Websocket-Origin" header and in 13 it's
195 # client sends a "Sec-Websocket-Origin" header and in 13 it's
194 # simply "Origin".
196 # simply "Origin".
195 if "Origin" in self.request.headers:
197 if "Origin" in self.request.headers:
196 origin = self.request.headers.get("Origin")
198 origin = self.request.headers.get("Origin")
197 else:
199 else:
198 origin = self.request.headers.get("Sec-Websocket-Origin", None)
200 origin = self.request.headers.get("Sec-Websocket-Origin", None)
199 return origin
201 return origin
200
202
201 #---------------------------------------------------------------
203 #---------------------------------------------------------------
202 # template rendering
204 # template rendering
203 #---------------------------------------------------------------
205 #---------------------------------------------------------------
204
206
205 def get_template(self, name):
207 def get_template(self, name):
206 """Return the jinja template object for a given name"""
208 """Return the jinja template object for a given name"""
207 return self.settings['jinja2_env'].get_template(name)
209 return self.settings['jinja2_env'].get_template(name)
208
210
209 def render_template(self, name, **ns):
211 def render_template(self, name, **ns):
210 ns.update(self.template_namespace)
212 ns.update(self.template_namespace)
211 template = self.get_template(name)
213 template = self.get_template(name)
212 return template.render(**ns)
214 return template.render(**ns)
213
215
214 @property
216 @property
215 def template_namespace(self):
217 def template_namespace(self):
216 return dict(
218 return dict(
217 base_url=self.base_url,
219 base_url=self.base_url,
218 ws_url=self.ws_url,
220 ws_url=self.ws_url,
219 logged_in=self.logged_in,
221 logged_in=self.logged_in,
220 login_available=self.login_available,
222 login_available=self.login_available,
221 static_url=self.static_url,
223 static_url=self.static_url,
222 )
224 )
223
225
224 def get_json_body(self):
226 def get_json_body(self):
225 """Return the body of the request as JSON data."""
227 """Return the body of the request as JSON data."""
226 if not self.request.body:
228 if not self.request.body:
227 return None
229 return None
228 # Do we need to call body.decode('utf-8') here?
230 # Do we need to call body.decode('utf-8') here?
229 body = self.request.body.strip().decode(u'utf-8')
231 body = self.request.body.strip().decode(u'utf-8')
230 try:
232 try:
231 model = json.loads(body)
233 model = json.loads(body)
232 except Exception:
234 except Exception:
233 self.log.debug("Bad JSON: %r", body)
235 self.log.debug("Bad JSON: %r", body)
234 self.log.error("Couldn't parse JSON", exc_info=True)
236 self.log.error("Couldn't parse JSON", exc_info=True)
235 raise web.HTTPError(400, u'Invalid JSON in body of request')
237 raise web.HTTPError(400, u'Invalid JSON in body of request')
236 return model
238 return model
237
239
238 def write_error(self, status_code, **kwargs):
240 def write_error(self, status_code, **kwargs):
239 """render custom error pages"""
241 """render custom error pages"""
240 exc_info = kwargs.get('exc_info')
242 exc_info = kwargs.get('exc_info')
241 message = ''
243 message = ''
242 status_message = responses.get(status_code, 'Unknown HTTP Error')
244 status_message = responses.get(status_code, 'Unknown HTTP Error')
243 if exc_info:
245 if exc_info:
244 exception = exc_info[1]
246 exception = exc_info[1]
245 # get the custom message, if defined
247 # get the custom message, if defined
246 try:
248 try:
247 message = exception.log_message % exception.args
249 message = exception.log_message % exception.args
248 except Exception:
250 except Exception:
249 pass
251 pass
250
252
251 # construct the custom reason, if defined
253 # construct the custom reason, if defined
252 reason = getattr(exception, 'reason', '')
254 reason = getattr(exception, 'reason', '')
253 if reason:
255 if reason:
254 status_message = reason
256 status_message = reason
255
257
256 # build template namespace
258 # build template namespace
257 ns = dict(
259 ns = dict(
258 status_code=status_code,
260 status_code=status_code,
259 status_message=status_message,
261 status_message=status_message,
260 message=message,
262 message=message,
261 exception=exception,
263 exception=exception,
262 )
264 )
263
265
264 self.set_header('Content-Type', 'text/html')
266 self.set_header('Content-Type', 'text/html')
265 # render the template
267 # render the template
266 try:
268 try:
267 html = self.render_template('%s.html' % status_code, **ns)
269 html = self.render_template('%s.html' % status_code, **ns)
268 except TemplateNotFound:
270 except TemplateNotFound:
269 self.log.debug("No template for %d", status_code)
271 self.log.debug("No template for %d", status_code)
270 html = self.render_template('error.html', **ns)
272 html = self.render_template('error.html', **ns)
271
273
272 self.write(html)
274 self.write(html)
273
275
274
276
275
277
276 class Template404(IPythonHandler):
278 class Template404(IPythonHandler):
277 """Render our 404 template"""
279 """Render our 404 template"""
278 def prepare(self):
280 def prepare(self):
279 raise web.HTTPError(404)
281 raise web.HTTPError(404)
280
282
281
283
282 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
284 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
283 """static files should only be accessible when logged in"""
285 """static files should only be accessible when logged in"""
284
286
285 @web.authenticated
287 @web.authenticated
286 def get(self, path):
288 def get(self, path):
287 if os.path.splitext(path)[1] == '.ipynb':
289 if os.path.splitext(path)[1] == '.ipynb':
288 name = os.path.basename(path)
290 name = os.path.basename(path)
289 self.set_header('Content-Type', 'application/json')
291 self.set_header('Content-Type', 'application/json')
290 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
292 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
291
293
292 return web.StaticFileHandler.get(self, path)
294 return web.StaticFileHandler.get(self, path)
293
295
294 def compute_etag(self):
296 def compute_etag(self):
295 return None
297 return None
296
298
297 def validate_absolute_path(self, root, absolute_path):
299 def validate_absolute_path(self, root, absolute_path):
298 """Validate and return the absolute path.
300 """Validate and return the absolute path.
299
301
300 Requires tornado 3.1
302 Requires tornado 3.1
301
303
302 Adding to tornado's own handling, forbids the serving of hidden files.
304 Adding to tornado's own handling, forbids the serving of hidden files.
303 """
305 """
304 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
306 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
305 abs_root = os.path.abspath(root)
307 abs_root = os.path.abspath(root)
306 if is_hidden(abs_path, abs_root):
308 if is_hidden(abs_path, abs_root):
307 self.log.info("Refusing to serve hidden file, via 404 Error")
309 self.log.info("Refusing to serve hidden file, via 404 Error")
308 raise web.HTTPError(404)
310 raise web.HTTPError(404)
309 return abs_path
311 return abs_path
310
312
311
313
312 def json_errors(method):
314 def json_errors(method):
313 """Decorate methods with this to return GitHub style JSON errors.
315 """Decorate methods with this to return GitHub style JSON errors.
314
316
315 This should be used on any JSON API on any handler method that can raise HTTPErrors.
317 This should be used on any JSON API on any handler method that can raise HTTPErrors.
316
318
317 This will grab the latest HTTPError exception using sys.exc_info
319 This will grab the latest HTTPError exception using sys.exc_info
318 and then:
320 and then:
319
321
320 1. Set the HTTP status code based on the HTTPError
322 1. Set the HTTP status code based on the HTTPError
321 2. Create and return a JSON body with a message field describing
323 2. Create and return a JSON body with a message field describing
322 the error in a human readable form.
324 the error in a human readable form.
323 """
325 """
324 @functools.wraps(method)
326 @functools.wraps(method)
325 def wrapper(self, *args, **kwargs):
327 def wrapper(self, *args, **kwargs):
326 try:
328 try:
327 result = method(self, *args, **kwargs)
329 result = method(self, *args, **kwargs)
328 except web.HTTPError as e:
330 except web.HTTPError as e:
329 status = e.status_code
331 status = e.status_code
330 message = e.log_message
332 message = e.log_message
331 self.log.warn(message)
333 self.log.warn(message)
332 self.set_status(e.status_code)
334 self.set_status(e.status_code)
333 self.finish(json.dumps(dict(message=message)))
335 self.finish(json.dumps(dict(message=message)))
334 except Exception:
336 except Exception:
335 self.log.error("Unhandled error in API request", exc_info=True)
337 self.log.error("Unhandled error in API request", exc_info=True)
336 status = 500
338 status = 500
337 message = "Unknown server error"
339 message = "Unknown server error"
338 t, value, tb = sys.exc_info()
340 t, value, tb = sys.exc_info()
339 self.set_status(status)
341 self.set_status(status)
340 tb_text = ''.join(traceback.format_exception(t, value, tb))
342 tb_text = ''.join(traceback.format_exception(t, value, tb))
341 reply = dict(message=message, traceback=tb_text)
343 reply = dict(message=message, traceback=tb_text)
342 self.finish(json.dumps(reply))
344 self.finish(json.dumps(reply))
343 else:
345 else:
344 return result
346 return result
345 return wrapper
347 return wrapper
346
348
347
349
348
350
349 #-----------------------------------------------------------------------------
351 #-----------------------------------------------------------------------------
350 # File handler
352 # File handler
351 #-----------------------------------------------------------------------------
353 #-----------------------------------------------------------------------------
352
354
353 # to minimize subclass changes:
355 # to minimize subclass changes:
354 HTTPError = web.HTTPError
356 HTTPError = web.HTTPError
355
357
356 class FileFindHandler(web.StaticFileHandler):
358 class FileFindHandler(web.StaticFileHandler):
357 """subclass of StaticFileHandler for serving files from a search path"""
359 """subclass of StaticFileHandler for serving files from a search path"""
358
360
359 # cache search results, don't search for files more than once
361 # cache search results, don't search for files more than once
360 _static_paths = {}
362 _static_paths = {}
361
363
362 def initialize(self, path, default_filename=None):
364 def initialize(self, path, default_filename=None):
363 if isinstance(path, string_types):
365 if isinstance(path, string_types):
364 path = [path]
366 path = [path]
365
367
366 self.root = tuple(
368 self.root = tuple(
367 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
369 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
368 )
370 )
369 self.default_filename = default_filename
371 self.default_filename = default_filename
370
372
371 def compute_etag(self):
373 def compute_etag(self):
372 return None
374 return None
373
375
374 @classmethod
376 @classmethod
375 def get_absolute_path(cls, roots, path):
377 def get_absolute_path(cls, roots, path):
376 """locate a file to serve on our static file search path"""
378 """locate a file to serve on our static file search path"""
377 with cls._lock:
379 with cls._lock:
378 if path in cls._static_paths:
380 if path in cls._static_paths:
379 return cls._static_paths[path]
381 return cls._static_paths[path]
380 try:
382 try:
381 abspath = os.path.abspath(filefind(path, roots))
383 abspath = os.path.abspath(filefind(path, roots))
382 except IOError:
384 except IOError:
383 # IOError means not found
385 # IOError means not found
384 return ''
386 return ''
385
387
386 cls._static_paths[path] = abspath
388 cls._static_paths[path] = abspath
387 return abspath
389 return abspath
388
390
389 def validate_absolute_path(self, root, absolute_path):
391 def validate_absolute_path(self, root, absolute_path):
390 """check if the file should be served (raises 404, 403, etc.)"""
392 """check if the file should be served (raises 404, 403, etc.)"""
391 if absolute_path == '':
393 if absolute_path == '':
392 raise web.HTTPError(404)
394 raise web.HTTPError(404)
393
395
394 for root in self.root:
396 for root in self.root:
395 if (absolute_path + os.sep).startswith(root):
397 if (absolute_path + os.sep).startswith(root):
396 break
398 break
397
399
398 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
400 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
399
401
400
402
403 class ApiVersionHandler(IPythonHandler):
404
405 @json_errors
406 def get(self):
407 # not authenticated, so give as few info as possible
408 self.finish(json.dumps({"version":IPython.__version__}))
409
401 class TrailingSlashHandler(web.RequestHandler):
410 class TrailingSlashHandler(web.RequestHandler):
402 """Simple redirect handler that strips trailing slashes
411 """Simple redirect handler that strips trailing slashes
403
412
404 This should be the first, highest priority handler.
413 This should be the first, highest priority handler.
405 """
414 """
406
415
407 SUPPORTED_METHODS = ['GET']
416 SUPPORTED_METHODS = ['GET']
408
417
409 def get(self):
418 def get(self):
410 self.redirect(self.request.uri.rstrip('/'))
419 self.redirect(self.request.uri.rstrip('/'))
411
420
412
421
413 class FilesRedirectHandler(IPythonHandler):
422 class FilesRedirectHandler(IPythonHandler):
414 """Handler for redirecting relative URLs to the /files/ handler"""
423 """Handler for redirecting relative URLs to the /files/ handler"""
415 def get(self, path=''):
424 def get(self, path=''):
416 cm = self.contents_manager
425 cm = self.contents_manager
417 if cm.path_exists(path):
426 if cm.path_exists(path):
418 # it's a *directory*, redirect to /tree
427 # it's a *directory*, redirect to /tree
419 url = url_path_join(self.base_url, 'tree', path)
428 url = url_path_join(self.base_url, 'tree', path)
420 else:
429 else:
421 orig_path = path
430 orig_path = path
422 # otherwise, redirect to /files
431 # otherwise, redirect to /files
423 parts = path.split('/')
432 parts = path.split('/')
424 path = '/'.join(parts[:-1])
433 path = '/'.join(parts[:-1])
425 name = parts[-1]
434 name = parts[-1]
426
435
427 if not cm.file_exists(name=name, path=path) and 'files' in parts:
436 if not cm.file_exists(name=name, path=path) and 'files' in parts:
428 # redirect without files/ iff it would 404
437 # redirect without files/ iff it would 404
429 # this preserves pre-2.0-style 'files/' links
438 # this preserves pre-2.0-style 'files/' links
430 self.log.warn("Deprecated files/ URL: %s", orig_path)
439 self.log.warn("Deprecated files/ URL: %s", orig_path)
431 parts.remove('files')
440 parts.remove('files')
432 path = '/'.join(parts[:-1])
441 path = '/'.join(parts[:-1])
433
442
434 if not cm.file_exists(name=name, path=path):
443 if not cm.file_exists(name=name, path=path):
435 raise web.HTTPError(404)
444 raise web.HTTPError(404)
436
445
437 url = url_path_join(self.base_url, 'files', path, name)
446 url = url_path_join(self.base_url, 'files', path, name)
438 url = url_escape(url)
447 url = url_escape(url)
439 self.log.debug("Redirecting %s to %s", self.request.path, url)
448 self.log.debug("Redirecting %s to %s", self.request.path, url)
440 self.redirect(url)
449 self.redirect(url)
441
450
442
451
443 #-----------------------------------------------------------------------------
452 #-----------------------------------------------------------------------------
444 # URL pattern fragments for re-use
453 # URL pattern fragments for re-use
445 #-----------------------------------------------------------------------------
454 #-----------------------------------------------------------------------------
446
455
447 path_regex = r"(?P<path>(?:/.*)*)"
456 path_regex = r"(?P<path>(?:/.*)*)"
448 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
457 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
449 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
458 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
450 file_name_regex = r"(?P<name>[^/]+)"
459 file_name_regex = r"(?P<name>[^/]+)"
451 file_path_regex = "%s/%s" % (path_regex, file_name_regex)
460 file_path_regex = "%s/%s" % (path_regex, file_name_regex)
452
461
453 #-----------------------------------------------------------------------------
462 #-----------------------------------------------------------------------------
454 # URL to handler mappings
463 # URL to handler mappings
455 #-----------------------------------------------------------------------------
464 #-----------------------------------------------------------------------------
456
465
457
466
458 default_handlers = [
467 default_handlers = [
459 (r".*/", TrailingSlashHandler)
468 (r".*/", TrailingSlashHandler),
469 (r"api", ApiVersionHandler)
460 ]
470 ]
General Comments 0
You need to be logged in to leave comments. Login now