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