##// END OF EJS Templates
cleanup weird 404
MinRK -
Show More
@@ -1,493 +1,492 b''
1 """Base Tornado handlers for the notebook.
1 """Base Tornado handlers for the notebook.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
9 # Copyright (C) 2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19
19
20 import datetime
20 import datetime
21 import email.utils
21 import email.utils
22 import functools
22 import functools
23 import hashlib
23 import hashlib
24 import json
24 import json
25 import logging
25 import logging
26 import mimetypes
26 import mimetypes
27 import os
27 import os
28 import stat
28 import stat
29 import sys
29 import sys
30 import threading
30 import threading
31 import traceback
31 import traceback
32
32
33 from tornado import web
33 from tornado import web
34 from tornado import websocket
34 from tornado import websocket
35
35
36 try:
36 try:
37 from tornado.log import app_log
37 from tornado.log import app_log
38 except ImportError:
38 except ImportError:
39 app_log = logging.getLogger()
39 app_log = logging.getLogger()
40
40
41 from IPython.config import Application
41 from IPython.config import Application
42 from IPython.external.decorator import decorator
42 from IPython.external.decorator import decorator
43 from IPython.utils.path import filefind
43 from IPython.utils.path import filefind
44 from IPython.utils.jsonutil import date_default
44 from IPython.utils.jsonutil import date_default
45
45
46 #-----------------------------------------------------------------------------
46 #-----------------------------------------------------------------------------
47 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
47 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
48 #-----------------------------------------------------------------------------
48 #-----------------------------------------------------------------------------
49
49
50 # Google Chrome, as of release 16, changed its websocket protocol number. The
50 # Google Chrome, as of release 16, changed its websocket protocol number. The
51 # parts tornado cares about haven't really changed, so it's OK to continue
51 # parts tornado cares about haven't really changed, so it's OK to continue
52 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
52 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
53 # version as of Oct 30/2011) the version check fails, see the issue report:
53 # version as of Oct 30/2011) the version check fails, see the issue report:
54
54
55 # https://github.com/facebook/tornado/issues/385
55 # https://github.com/facebook/tornado/issues/385
56
56
57 # This issue has been fixed in Tornado post 2.1.1:
57 # This issue has been fixed in Tornado post 2.1.1:
58
58
59 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
59 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
60
60
61 # Here we manually apply the same patch as above so that users of IPython can
61 # Here we manually apply the same patch as above so that users of IPython can
62 # continue to work with an officially released Tornado. We make the
62 # continue to work with an officially released Tornado. We make the
63 # monkeypatch version check as narrow as possible to limit its effects; once
63 # monkeypatch version check as narrow as possible to limit its effects; once
64 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
64 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
65
65
66 import tornado
66 import tornado
67
67
68 if tornado.version_info <= (2,1,1):
68 if tornado.version_info <= (2,1,1):
69
69
70 def _execute(self, transforms, *args, **kwargs):
70 def _execute(self, transforms, *args, **kwargs):
71 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
71 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
72
72
73 self.open_args = args
73 self.open_args = args
74 self.open_kwargs = kwargs
74 self.open_kwargs = kwargs
75
75
76 # The difference between version 8 and 13 is that in 8 the
76 # The difference between version 8 and 13 is that in 8 the
77 # client sends a "Sec-Websocket-Origin" header and in 13 it's
77 # client sends a "Sec-Websocket-Origin" header and in 13 it's
78 # simply "Origin".
78 # simply "Origin".
79 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
79 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
80 self.ws_connection = WebSocketProtocol8(self)
80 self.ws_connection = WebSocketProtocol8(self)
81 self.ws_connection.accept_connection()
81 self.ws_connection.accept_connection()
82
82
83 elif self.request.headers.get("Sec-WebSocket-Version"):
83 elif self.request.headers.get("Sec-WebSocket-Version"):
84 self.stream.write(tornado.escape.utf8(
84 self.stream.write(tornado.escape.utf8(
85 "HTTP/1.1 426 Upgrade Required\r\n"
85 "HTTP/1.1 426 Upgrade Required\r\n"
86 "Sec-WebSocket-Version: 8\r\n\r\n"))
86 "Sec-WebSocket-Version: 8\r\n\r\n"))
87 self.stream.close()
87 self.stream.close()
88
88
89 else:
89 else:
90 self.ws_connection = WebSocketProtocol76(self)
90 self.ws_connection = WebSocketProtocol76(self)
91 self.ws_connection.accept_connection()
91 self.ws_connection.accept_connection()
92
92
93 websocket.WebSocketHandler._execute = _execute
93 websocket.WebSocketHandler._execute = _execute
94 del _execute
94 del _execute
95
95
96
96
97 #-----------------------------------------------------------------------------
97 #-----------------------------------------------------------------------------
98 # Top-level handlers
98 # Top-level handlers
99 #-----------------------------------------------------------------------------
99 #-----------------------------------------------------------------------------
100
100
101 class RequestHandler(web.RequestHandler):
101 class RequestHandler(web.RequestHandler):
102 """RequestHandler with default variable setting."""
102 """RequestHandler with default variable setting."""
103
103
104 def render(*args, **kwargs):
104 def render(*args, **kwargs):
105 kwargs.setdefault('message', '')
105 kwargs.setdefault('message', '')
106 return web.RequestHandler.render(*args, **kwargs)
106 return web.RequestHandler.render(*args, **kwargs)
107
107
108 class AuthenticatedHandler(RequestHandler):
108 class AuthenticatedHandler(RequestHandler):
109 """A RequestHandler with an authenticated user."""
109 """A RequestHandler with an authenticated user."""
110
110
111 def clear_login_cookie(self):
111 def clear_login_cookie(self):
112 self.clear_cookie(self.cookie_name)
112 self.clear_cookie(self.cookie_name)
113
113
114 def get_current_user(self):
114 def get_current_user(self):
115 user_id = self.get_secure_cookie(self.cookie_name)
115 user_id = self.get_secure_cookie(self.cookie_name)
116 # For now the user_id should not return empty, but it could eventually
116 # For now the user_id should not return empty, but it could eventually
117 if user_id == '':
117 if user_id == '':
118 user_id = 'anonymous'
118 user_id = 'anonymous'
119 if user_id is None:
119 if user_id is None:
120 # prevent extra Invalid cookie sig warnings:
120 # prevent extra Invalid cookie sig warnings:
121 self.clear_login_cookie()
121 self.clear_login_cookie()
122 if not self.login_available:
122 if not self.login_available:
123 user_id = 'anonymous'
123 user_id = 'anonymous'
124 return user_id
124 return user_id
125
125
126 @property
126 @property
127 def cookie_name(self):
127 def cookie_name(self):
128 default_cookie_name = 'username-{host}'.format(
128 default_cookie_name = 'username-{host}'.format(
129 host=self.request.host,
129 host=self.request.host,
130 ).replace(':', '-')
130 ).replace(':', '-')
131 return self.settings.get('cookie_name', default_cookie_name)
131 return self.settings.get('cookie_name', default_cookie_name)
132
132
133 @property
133 @property
134 def password(self):
134 def password(self):
135 """our password"""
135 """our password"""
136 return self.settings.get('password', '')
136 return self.settings.get('password', '')
137
137
138 @property
138 @property
139 def logged_in(self):
139 def logged_in(self):
140 """Is a user currently logged in?
140 """Is a user currently logged in?
141
141
142 """
142 """
143 user = self.get_current_user()
143 user = self.get_current_user()
144 return (user and not user == 'anonymous')
144 return (user and not user == 'anonymous')
145
145
146 @property
146 @property
147 def login_available(self):
147 def login_available(self):
148 """May a user proceed to log in?
148 """May a user proceed to log in?
149
149
150 This returns True if login capability is available, irrespective of
150 This returns True if login capability is available, irrespective of
151 whether the user is already logged in or not.
151 whether the user is already logged in or not.
152
152
153 """
153 """
154 return bool(self.settings.get('password', ''))
154 return bool(self.settings.get('password', ''))
155
155
156
156
157 class IPythonHandler(AuthenticatedHandler):
157 class IPythonHandler(AuthenticatedHandler):
158 """IPython-specific extensions to authenticated handling
158 """IPython-specific extensions to authenticated handling
159
159
160 Mostly property shortcuts to IPython-specific settings.
160 Mostly property shortcuts to IPython-specific settings.
161 """
161 """
162
162
163 @property
163 @property
164 def config(self):
164 def config(self):
165 return self.settings.get('config', None)
165 return self.settings.get('config', None)
166
166
167 @property
167 @property
168 def log(self):
168 def log(self):
169 """use the IPython log by default, falling back on tornado's logger"""
169 """use the IPython log by default, falling back on tornado's logger"""
170 if Application.initialized():
170 if Application.initialized():
171 return Application.instance().log
171 return Application.instance().log
172 else:
172 else:
173 return app_log
173 return app_log
174
174
175 @property
175 @property
176 def use_less(self):
176 def use_less(self):
177 """Use less instead of css in templates"""
177 """Use less instead of css in templates"""
178 return self.settings.get('use_less', False)
178 return self.settings.get('use_less', False)
179
179
180 #---------------------------------------------------------------
180 #---------------------------------------------------------------
181 # URLs
181 # URLs
182 #---------------------------------------------------------------
182 #---------------------------------------------------------------
183
183
184 @property
184 @property
185 def ws_url(self):
185 def ws_url(self):
186 """websocket url matching the current request
186 """websocket url matching the current request
187
187
188 By default, this is just `''`, indicating that it should match
188 By default, this is just `''`, indicating that it should match
189 the same host, protocol, port, etc.
189 the same host, protocol, port, etc.
190 """
190 """
191 return self.settings.get('websocket_url', '')
191 return self.settings.get('websocket_url', '')
192
192
193 @property
193 @property
194 def mathjax_url(self):
194 def mathjax_url(self):
195 return self.settings.get('mathjax_url', '')
195 return self.settings.get('mathjax_url', '')
196
196
197 @property
197 @property
198 def base_project_url(self):
198 def base_project_url(self):
199 return self.settings.get('base_project_url', '/')
199 return self.settings.get('base_project_url', '/')
200
200
201 @property
201 @property
202 def base_kernel_url(self):
202 def base_kernel_url(self):
203 return self.settings.get('base_kernel_url', '/')
203 return self.settings.get('base_kernel_url', '/')
204
204
205 #---------------------------------------------------------------
205 #---------------------------------------------------------------
206 # Manager objects
206 # Manager objects
207 #---------------------------------------------------------------
207 #---------------------------------------------------------------
208
208
209 @property
209 @property
210 def kernel_manager(self):
210 def kernel_manager(self):
211 return self.settings['kernel_manager']
211 return self.settings['kernel_manager']
212
212
213 @property
213 @property
214 def notebook_manager(self):
214 def notebook_manager(self):
215 return self.settings['notebook_manager']
215 return self.settings['notebook_manager']
216
216
217 @property
217 @property
218 def cluster_manager(self):
218 def cluster_manager(self):
219 return self.settings['cluster_manager']
219 return self.settings['cluster_manager']
220
220
221 @property
221 @property
222 def session_manager(self):
222 def session_manager(self):
223 return self.settings['session_manager']
223 return self.settings['session_manager']
224
224
225 @property
225 @property
226 def project_dir(self):
226 def project_dir(self):
227 return self.notebook_manager.notebook_dir
227 return self.notebook_manager.notebook_dir
228
228
229 #---------------------------------------------------------------
229 #---------------------------------------------------------------
230 # template rendering
230 # template rendering
231 #---------------------------------------------------------------
231 #---------------------------------------------------------------
232
232
233 def get_template(self, name):
233 def get_template(self, name):
234 """Return the jinja template object for a given name"""
234 """Return the jinja template object for a given name"""
235 return self.settings['jinja2_env'].get_template(name)
235 return self.settings['jinja2_env'].get_template(name)
236
236
237 def render_template(self, name, **ns):
237 def render_template(self, name, **ns):
238 ns.update(self.template_namespace)
238 ns.update(self.template_namespace)
239 template = self.get_template(name)
239 template = self.get_template(name)
240 return template.render(**ns)
240 return template.render(**ns)
241
241
242 @property
242 @property
243 def template_namespace(self):
243 def template_namespace(self):
244 return dict(
244 return dict(
245 base_project_url=self.base_project_url,
245 base_project_url=self.base_project_url,
246 base_kernel_url=self.base_kernel_url,
246 base_kernel_url=self.base_kernel_url,
247 logged_in=self.logged_in,
247 logged_in=self.logged_in,
248 login_available=self.login_available,
248 login_available=self.login_available,
249 use_less=self.use_less,
249 use_less=self.use_less,
250 )
250 )
251
251
252 def get_json_body(self):
252 def get_json_body(self):
253 """Return the body of the request as JSON data."""
253 """Return the body of the request as JSON data."""
254 if not self.request.body:
254 if not self.request.body:
255 return None
255 return None
256 # Do we need to call body.decode('utf-8') here?
256 # Do we need to call body.decode('utf-8') here?
257 body = self.request.body.strip().decode(u'utf-8')
257 body = self.request.body.strip().decode(u'utf-8')
258 try:
258 try:
259 model = json.loads(body)
259 model = json.loads(body)
260 except Exception:
260 except Exception:
261 self.log.debug("Bad JSON: %r", body)
261 self.log.debug("Bad JSON: %r", body)
262 self.log.error("Couldn't parse JSON", exc_info=True)
262 self.log.error("Couldn't parse JSON", exc_info=True)
263 raise web.HTTPError(400, u'Invalid JSON in body of request')
263 raise web.HTTPError(400, u'Invalid JSON in body of request')
264 return model
264 return model
265
265
266 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
266 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
267 """static files should only be accessible when logged in"""
267 """static files should only be accessible when logged in"""
268
268
269 @web.authenticated
269 @web.authenticated
270 def get(self, path):
270 def get(self, path):
271 return web.StaticFileHandler.get(self, path)
271 return web.StaticFileHandler.get(self, path)
272
272
273
273
274 def json_errors(method):
274 def json_errors(method):
275 """Decorate methods with this to return GitHub style JSON errors.
275 """Decorate methods with this to return GitHub style JSON errors.
276
276
277 This should be used on any JSON API on any handler method that can raise HTTPErrors.
277 This should be used on any JSON API on any handler method that can raise HTTPErrors.
278
278
279 This will grab the latest HTTPError exception using sys.exc_info
279 This will grab the latest HTTPError exception using sys.exc_info
280 and then:
280 and then:
281
281
282 1. Set the HTTP status code based on the HTTPError
282 1. Set the HTTP status code based on the HTTPError
283 2. Create and return a JSON body with a message field describing
283 2. Create and return a JSON body with a message field describing
284 the error in a human readable form.
284 the error in a human readable form.
285 """
285 """
286 @functools.wraps(method)
286 @functools.wraps(method)
287 def wrapper(self, *args, **kwargs):
287 def wrapper(self, *args, **kwargs):
288 try:
288 try:
289 result = method(self, *args, **kwargs)
289 result = method(self, *args, **kwargs)
290 except web.HTTPError as e:
290 except web.HTTPError as e:
291 status = e.status_code
291 status = e.status_code
292 message = e.log_message
292 message = e.log_message
293 self.set_status(e.status_code)
293 self.set_status(e.status_code)
294 self.finish(json.dumps(dict(message=message)))
294 self.finish(json.dumps(dict(message=message)))
295 except Exception:
295 except Exception:
296 self.log.error("Unhandled error in API request", exc_info=True)
296 self.log.error("Unhandled error in API request", exc_info=True)
297 status = 500
297 status = 500
298 message = "Unknown server error"
298 message = "Unknown server error"
299 t, value, tb = sys.exc_info()
299 t, value, tb = sys.exc_info()
300 self.set_status(status)
300 self.set_status(status)
301 tb_text = ''.join(traceback.format_exception(t, value, tb))
301 tb_text = ''.join(traceback.format_exception(t, value, tb))
302 reply = dict(message=message, traceback=tb_text)
302 reply = dict(message=message, traceback=tb_text)
303 self.finish(json.dumps(reply))
303 self.finish(json.dumps(reply))
304 else:
304 else:
305 return result
305 return result
306 return wrapper
306 return wrapper
307
307
308
308
309
309
310 #-----------------------------------------------------------------------------
310 #-----------------------------------------------------------------------------
311 # File handler
311 # File handler
312 #-----------------------------------------------------------------------------
312 #-----------------------------------------------------------------------------
313
313
314 # to minimize subclass changes:
314 # to minimize subclass changes:
315 HTTPError = web.HTTPError
315 HTTPError = web.HTTPError
316
316
317 class FileFindHandler(web.StaticFileHandler):
317 class FileFindHandler(web.StaticFileHandler):
318 """subclass of StaticFileHandler for serving files from a search path"""
318 """subclass of StaticFileHandler for serving files from a search path"""
319
319
320 _static_paths = {}
320 _static_paths = {}
321 # _lock is needed for tornado < 2.2.0 compat
321 # _lock is needed for tornado < 2.2.0 compat
322 _lock = threading.Lock() # protects _static_hashes
322 _lock = threading.Lock() # protects _static_hashes
323
323
324 def initialize(self, path, default_filename=None):
324 def initialize(self, path, default_filename=None):
325 if isinstance(path, basestring):
325 if isinstance(path, basestring):
326 path = [path]
326 path = [path]
327 self.roots = tuple(
327 self.roots = tuple(
328 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
328 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
329 )
329 )
330 self.default_filename = default_filename
330 self.default_filename = default_filename
331
331
332 @classmethod
332 @classmethod
333 def locate_file(cls, path, roots):
333 def locate_file(cls, path, roots):
334 """locate a file to serve on our static file search path"""
334 """locate a file to serve on our static file search path"""
335 with cls._lock:
335 with cls._lock:
336 if path in cls._static_paths:
336 if path in cls._static_paths:
337 return cls._static_paths[path]
337 return cls._static_paths[path]
338 try:
338 try:
339 abspath = os.path.abspath(filefind(path, roots))
339 abspath = os.path.abspath(filefind(path, roots))
340 except IOError:
340 except IOError:
341 # empty string should always give exists=False
341 # empty string should always give exists=False
342 return ''
342 return ''
343
343
344 # os.path.abspath strips a trailing /
344 # os.path.abspath strips a trailing /
345 # it needs to be temporarily added back for requests to root/
345 # it needs to be temporarily added back for requests to root/
346 if not (abspath + os.path.sep).startswith(roots):
346 if not (abspath + os.path.sep).startswith(roots):
347 raise HTTPError(403, "%s is not in root static directory", path)
347 raise HTTPError(403, "%s is not in root static directory", path)
348
348
349 cls._static_paths[path] = abspath
349 cls._static_paths[path] = abspath
350 return abspath
350 return abspath
351
351
352 def get(self, path, include_body=True):
352 def get(self, path, include_body=True):
353 path = self.parse_url_path(path)
353 path = self.parse_url_path(path)
354
354
355 # begin subclass override
355 # begin subclass override
356 abspath = self.locate_file(path, self.roots)
356 abspath = self.locate_file(path, self.roots)
357 # end subclass override
357 # end subclass override
358
358
359 if os.path.isdir(abspath) and self.default_filename is not None:
359 if os.path.isdir(abspath) and self.default_filename is not None:
360 # need to look at the request.path here for when path is empty
360 # need to look at the request.path here for when path is empty
361 # but there is some prefix to the path that was already
361 # but there is some prefix to the path that was already
362 # trimmed by the routing
362 # trimmed by the routing
363 if not self.request.path.endswith("/"):
363 if not self.request.path.endswith("/"):
364 self.redirect(self.request.path + "/")
364 self.redirect(self.request.path + "/")
365 return
365 return
366 abspath = os.path.join(abspath, self.default_filename)
366 abspath = os.path.join(abspath, self.default_filename)
367 if not os.path.exists(abspath):
367 if not os.path.exists(abspath):
368 raise HTTPError(404)
368 raise HTTPError(404)
369 if not os.path.isfile(abspath):
369 if not os.path.isfile(abspath):
370 raise HTTPError(403, "%s is not a file", path)
370 raise HTTPError(403, "%s is not a file", path)
371
371
372 stat_result = os.stat(abspath)
372 stat_result = os.stat(abspath)
373 modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
373 modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
374
374
375 self.set_header("Last-Modified", modified)
375 self.set_header("Last-Modified", modified)
376
376
377 mime_type, encoding = mimetypes.guess_type(abspath)
377 mime_type, encoding = mimetypes.guess_type(abspath)
378 if mime_type:
378 if mime_type:
379 self.set_header("Content-Type", mime_type)
379 self.set_header("Content-Type", mime_type)
380
380
381 cache_time = self.get_cache_time(path, modified, mime_type)
381 cache_time = self.get_cache_time(path, modified, mime_type)
382
382
383 if cache_time > 0:
383 if cache_time > 0:
384 self.set_header("Expires", datetime.datetime.utcnow() + \
384 self.set_header("Expires", datetime.datetime.utcnow() + \
385 datetime.timedelta(seconds=cache_time))
385 datetime.timedelta(seconds=cache_time))
386 self.set_header("Cache-Control", "max-age=" + str(cache_time))
386 self.set_header("Cache-Control", "max-age=" + str(cache_time))
387 else:
387 else:
388 self.set_header("Cache-Control", "public")
388 self.set_header("Cache-Control", "public")
389
389
390 self.set_extra_headers(path)
390 self.set_extra_headers(path)
391
391
392 # Check the If-Modified-Since, and don't send the result if the
392 # Check the If-Modified-Since, and don't send the result if the
393 # content has not been modified
393 # content has not been modified
394 ims_value = self.request.headers.get("If-Modified-Since")
394 ims_value = self.request.headers.get("If-Modified-Since")
395 if ims_value is not None:
395 if ims_value is not None:
396 date_tuple = email.utils.parsedate(ims_value)
396 date_tuple = email.utils.parsedate(ims_value)
397 if_since = datetime.datetime(*date_tuple[:6])
397 if_since = datetime.datetime(*date_tuple[:6])
398 if if_since >= modified:
398 if if_since >= modified:
399 self.set_status(304)
399 self.set_status(304)
400 return
400 return
401
401
402 if os.path.splitext(path)[1] == '.ipynb':
402 if os.path.splitext(path)[1] == '.ipynb':
403 raise HTTPError(404, 'HAHA')
403 name = os.path.basename(path)
404 name = os.path.splitext(os.path.split(path))[0]
405 self.set_header('Content-Type', 'application/json')
404 self.set_header('Content-Type', 'application/json')
406 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
405 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
407
406
408 with open(abspath, "rb") as file:
407 with open(abspath, "rb") as file:
409 data = file.read()
408 data = file.read()
410 hasher = hashlib.sha1()
409 hasher = hashlib.sha1()
411 hasher.update(data)
410 hasher.update(data)
412 self.set_header("Etag", '"%s"' % hasher.hexdigest())
411 self.set_header("Etag", '"%s"' % hasher.hexdigest())
413 if include_body:
412 if include_body:
414 self.write(data)
413 self.write(data)
415 else:
414 else:
416 assert self.request.method == "HEAD"
415 assert self.request.method == "HEAD"
417 self.set_header("Content-Length", len(data))
416 self.set_header("Content-Length", len(data))
418
417
419 @classmethod
418 @classmethod
420 def get_version(cls, settings, path):
419 def get_version(cls, settings, path):
421 """Generate the version string to be used in static URLs.
420 """Generate the version string to be used in static URLs.
422
421
423 This method may be overridden in subclasses (but note that it
422 This method may be overridden in subclasses (but note that it
424 is a class method rather than a static method). The default
423 is a class method rather than a static method). The default
425 implementation uses a hash of the file's contents.
424 implementation uses a hash of the file's contents.
426
425
427 ``settings`` is the `Application.settings` dictionary and ``path``
426 ``settings`` is the `Application.settings` dictionary and ``path``
428 is the relative location of the requested asset on the filesystem.
427 is the relative location of the requested asset on the filesystem.
429 The returned value should be a string, or ``None`` if no version
428 The returned value should be a string, or ``None`` if no version
430 could be determined.
429 could be determined.
431 """
430 """
432 # begin subclass override:
431 # begin subclass override:
433 static_paths = settings['static_path']
432 static_paths = settings['static_path']
434 if isinstance(static_paths, basestring):
433 if isinstance(static_paths, basestring):
435 static_paths = [static_paths]
434 static_paths = [static_paths]
436 roots = tuple(
435 roots = tuple(
437 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
436 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
438 )
437 )
439
438
440 try:
439 try:
441 abs_path = filefind(path, roots)
440 abs_path = filefind(path, roots)
442 except IOError:
441 except IOError:
443 app_log.error("Could not find static file %r", path)
442 app_log.error("Could not find static file %r", path)
444 return None
443 return None
445
444
446 # end subclass override
445 # end subclass override
447
446
448 with cls._lock:
447 with cls._lock:
449 hashes = cls._static_hashes
448 hashes = cls._static_hashes
450 if abs_path not in hashes:
449 if abs_path not in hashes:
451 try:
450 try:
452 f = open(abs_path, "rb")
451 f = open(abs_path, "rb")
453 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
452 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
454 f.close()
453 f.close()
455 except Exception:
454 except Exception:
456 app_log.error("Could not open static file %r", path)
455 app_log.error("Could not open static file %r", path)
457 hashes[abs_path] = None
456 hashes[abs_path] = None
458 hsh = hashes.get(abs_path)
457 hsh = hashes.get(abs_path)
459 if hsh:
458 if hsh:
460 return hsh[:5]
459 return hsh[:5]
461 return None
460 return None
462
461
463
462
464 def parse_url_path(self, url_path):
463 def parse_url_path(self, url_path):
465 """Converts a static URL path into a filesystem path.
464 """Converts a static URL path into a filesystem path.
466
465
467 ``url_path`` is the path component of the URL with
466 ``url_path`` is the path component of the URL with
468 ``static_url_prefix`` removed. The return value should be
467 ``static_url_prefix`` removed. The return value should be
469 filesystem path relative to ``static_path``.
468 filesystem path relative to ``static_path``.
470 """
469 """
471 if os.path.sep != "/":
470 if os.path.sep != "/":
472 url_path = url_path.replace("/", os.path.sep)
471 url_path = url_path.replace("/", os.path.sep)
473 return url_path
472 return url_path
474
473
475 class TrailingSlashHandler(web.RequestHandler):
474 class TrailingSlashHandler(web.RequestHandler):
476 """Simple redirect handler that strips trailing slashes
475 """Simple redirect handler that strips trailing slashes
477
476
478 This should be the first, highest priority handler.
477 This should be the first, highest priority handler.
479 """
478 """
480
479
481 SUPPORTED_METHODS = ['GET']
480 SUPPORTED_METHODS = ['GET']
482
481
483 def get(self):
482 def get(self):
484 self.redirect(self.request.uri.rstrip('/'))
483 self.redirect(self.request.uri.rstrip('/'))
485
484
486 #-----------------------------------------------------------------------------
485 #-----------------------------------------------------------------------------
487 # URL to handler mappings
486 # URL to handler mappings
488 #-----------------------------------------------------------------------------
487 #-----------------------------------------------------------------------------
489
488
490
489
491 default_handlers = [
490 default_handlers = [
492 (r".*/", TrailingSlashHandler)
491 (r".*/", TrailingSlashHandler)
493 ]
492 ]
General Comments 0
You need to be logged in to leave comments. Login now