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