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