Removed spurious static_path.
[smonitor.git] / monitor / cherrypy / lib / static.py
blobcb9a68cbadfcfdaf17d5324ad2036b603408e9cc
1 import logging
2 import mimetypes
3 mimetypes.init()
4 mimetypes.types_map['.dwg']='image/x-dwg'
5 mimetypes.types_map['.ico']='image/x-icon'
6 mimetypes.types_map['.bz2']='application/x-bzip2'
7 mimetypes.types_map['.gz']='application/x-gzip'
9 import os
10 import re
11 import stat
12 import time
14 import cherrypy
15 from cherrypy._cpcompat import ntob, unquote
16 from cherrypy.lib import cptools, httputil, file_generator_limited
19 def serve_file(path, content_type=None, disposition=None, name=None, debug=False):
20 """Set status, headers, and body in order to serve the given path.
22 The Content-Type header will be set to the content_type arg, if provided.
23 If not provided, the Content-Type will be guessed by the file extension
24 of the 'path' argument.
26 If disposition is not None, the Content-Disposition header will be set
27 to "<disposition>; filename=<name>". If name is None, it will be set
28 to the basename of path. If disposition is None, no Content-Disposition
29 header will be written.
30 """
32 response = cherrypy.serving.response
34 # If path is relative, users should fix it by making path absolute.
35 # That is, CherryPy should not guess where the application root is.
36 # It certainly should *not* use cwd (since CP may be invoked from a
37 # variety of paths). If using tools.staticdir, you can make your relative
38 # paths become absolute by supplying a value for "tools.staticdir.root".
39 if not os.path.isabs(path):
40 msg = "'%s' is not an absolute path." % path
41 if debug:
42 cherrypy.log(msg, 'TOOLS.STATICFILE')
43 raise ValueError(msg)
45 try:
46 st = os.stat(path)
47 except OSError:
48 if debug:
49 cherrypy.log('os.stat(%r) failed' % path, 'TOOLS.STATIC')
50 raise cherrypy.NotFound()
52 # Check if path is a directory.
53 if stat.S_ISDIR(st.st_mode):
54 # Let the caller deal with it as they like.
55 if debug:
56 cherrypy.log('%r is a directory' % path, 'TOOLS.STATIC')
57 raise cherrypy.NotFound()
59 # Set the Last-Modified response header, so that
60 # modified-since validation code can work.
61 response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime)
62 cptools.validate_since()
64 if content_type is None:
65 # Set content-type based on filename extension
66 ext = ""
67 i = path.rfind('.')
68 if i != -1:
69 ext = path[i:].lower()
70 content_type = mimetypes.types_map.get(ext, None)
71 if content_type is not None:
72 response.headers['Content-Type'] = content_type
73 if debug:
74 cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC')
76 cd = None
77 if disposition is not None:
78 if name is None:
79 name = os.path.basename(path)
80 cd = '%s; filename="%s"' % (disposition, name)
81 response.headers["Content-Disposition"] = cd
82 if debug:
83 cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
85 # Set Content-Length and use an iterable (file object)
86 # this way CP won't load the whole file in memory
87 content_length = st.st_size
88 fileobj = open(path, 'rb')
89 return _serve_fileobj(fileobj, content_type, content_length, debug=debug)
91 def serve_fileobj(fileobj, content_type=None, disposition=None, name=None,
92 debug=False):
93 """Set status, headers, and body in order to serve the given file object.
95 The Content-Type header will be set to the content_type arg, if provided.
97 If disposition is not None, the Content-Disposition header will be set
98 to "<disposition>; filename=<name>". If name is None, 'filename' will
99 not be set. If disposition is None, no Content-Disposition header will
100 be written.
102 CAUTION: If the request contains a 'Range' header, one or more seek()s will
103 be performed on the file object. This may cause undesired behavior if
104 the file object is not seekable. It could also produce undesired results
105 if the caller set the read position of the file object prior to calling
106 serve_fileobj(), expecting that the data would be served starting from that
107 position.
110 response = cherrypy.serving.response
112 try:
113 st = os.fstat(fileobj.fileno())
114 except AttributeError:
115 if debug:
116 cherrypy.log('os has no fstat attribute', 'TOOLS.STATIC')
117 content_length = None
118 else:
119 # Set the Last-Modified response header, so that
120 # modified-since validation code can work.
121 response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime)
122 cptools.validate_since()
123 content_length = st.st_size
125 if content_type is not None:
126 response.headers['Content-Type'] = content_type
127 if debug:
128 cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC')
130 cd = None
131 if disposition is not None:
132 if name is None:
133 cd = disposition
134 else:
135 cd = '%s; filename="%s"' % (disposition, name)
136 response.headers["Content-Disposition"] = cd
137 if debug:
138 cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
140 return _serve_fileobj(fileobj, content_type, content_length, debug=debug)
142 def _serve_fileobj(fileobj, content_type, content_length, debug=False):
143 """Internal. Set response.body to the given file object, perhaps ranged."""
144 response = cherrypy.serving.response
146 # HTTP/1.0 didn't have Range/Accept-Ranges headers, or the 206 code
147 request = cherrypy.serving.request
148 if request.protocol >= (1, 1):
149 response.headers["Accept-Ranges"] = "bytes"
150 r = httputil.get_ranges(request.headers.get('Range'), content_length)
151 if r == []:
152 response.headers['Content-Range'] = "bytes */%s" % content_length
153 message = "Invalid Range (first-byte-pos greater than Content-Length)"
154 if debug:
155 cherrypy.log(message, 'TOOLS.STATIC')
156 raise cherrypy.HTTPError(416, message)
158 if r:
159 if len(r) == 1:
160 # Return a single-part response.
161 start, stop = r[0]
162 if stop > content_length:
163 stop = content_length
164 r_len = stop - start
165 if debug:
166 cherrypy.log('Single part; start: %r, stop: %r' % (start, stop),
167 'TOOLS.STATIC')
168 response.status = "206 Partial Content"
169 response.headers['Content-Range'] = (
170 "bytes %s-%s/%s" % (start, stop - 1, content_length))
171 response.headers['Content-Length'] = r_len
172 fileobj.seek(start)
173 response.body = file_generator_limited(fileobj, r_len)
174 else:
175 # Return a multipart/byteranges response.
176 response.status = "206 Partial Content"
177 from mimetools import choose_boundary
178 boundary = choose_boundary()
179 ct = "multipart/byteranges; boundary=%s" % boundary
180 response.headers['Content-Type'] = ct
181 if "Content-Length" in response.headers:
182 # Delete Content-Length header so finalize() recalcs it.
183 del response.headers["Content-Length"]
185 def file_ranges():
186 # Apache compatibility:
187 yield ntob("\r\n")
189 for start, stop in r:
190 if debug:
191 cherrypy.log('Multipart; start: %r, stop: %r' % (start, stop),
192 'TOOLS.STATIC')
193 yield ntob("--" + boundary, 'ascii')
194 yield ntob("\r\nContent-type: %s" % content_type, 'ascii')
195 yield ntob("\r\nContent-range: bytes %s-%s/%s\r\n\r\n"
196 % (start, stop - 1, content_length), 'ascii')
197 fileobj.seek(start)
198 for chunk in file_generator_limited(fileobj, stop-start):
199 yield chunk
200 yield ntob("\r\n")
201 # Final boundary
202 yield ntob("--" + boundary + "--", 'ascii')
204 # Apache compatibility:
205 yield ntob("\r\n")
206 response.body = file_ranges()
207 return response.body
208 else:
209 if debug:
210 cherrypy.log('No byteranges requested', 'TOOLS.STATIC')
212 # Set Content-Length and use an iterable (file object)
213 # this way CP won't load the whole file in memory
214 response.headers['Content-Length'] = content_length
215 response.body = fileobj
216 return response.body
218 def serve_download(path, name=None):
219 """Serve 'path' as an application/x-download attachment."""
220 # This is such a common idiom I felt it deserved its own wrapper.
221 return serve_file(path, "application/x-download", "attachment", name)
224 def _attempt(filename, content_types, debug=False):
225 if debug:
226 cherrypy.log('Attempting %r (content_types %r)' %
227 (filename, content_types), 'TOOLS.STATICDIR')
228 try:
229 # you can set the content types for a
230 # complete directory per extension
231 content_type = None
232 if content_types:
233 r, ext = os.path.splitext(filename)
234 content_type = content_types.get(ext[1:], None)
235 serve_file(filename, content_type=content_type, debug=debug)
236 return True
237 except cherrypy.NotFound:
238 # If we didn't find the static file, continue handling the
239 # request. We might find a dynamic handler instead.
240 if debug:
241 cherrypy.log('NotFound', 'TOOLS.STATICFILE')
242 return False
244 def staticdir(section, dir, root="", match="", content_types=None, index="",
245 debug=False):
246 """Serve a static resource from the given (root +) dir.
248 match
249 If given, request.path_info will be searched for the given
250 regular expression before attempting to serve static content.
252 content_types
253 If given, it should be a Python dictionary of
254 {file-extension: content-type} pairs, where 'file-extension' is
255 a string (e.g. "gif") and 'content-type' is the value to write
256 out in the Content-Type response header (e.g. "image/gif").
258 index
259 If provided, it should be the (relative) name of a file to
260 serve for directory requests. For example, if the dir argument is
261 '/home/me', the Request-URI is 'myapp', and the index arg is
262 'index.html', the file '/home/me/myapp/index.html' will be sought.
264 request = cherrypy.serving.request
265 if request.method not in ('GET', 'HEAD'):
266 if debug:
267 cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICDIR')
268 return False
270 if match and not re.search(match, request.path_info):
271 if debug:
272 cherrypy.log('request.path_info %r does not match pattern %r' %
273 (request.path_info, match), 'TOOLS.STATICDIR')
274 return False
276 # Allow the use of '~' to refer to a user's home directory.
277 dir = os.path.expanduser(dir)
279 # If dir is relative, make absolute using "root".
280 if not os.path.isabs(dir):
281 if not root:
282 msg = "Static dir requires an absolute dir (or root)."
283 if debug:
284 cherrypy.log(msg, 'TOOLS.STATICDIR')
285 raise ValueError(msg)
286 dir = os.path.join(root, dir)
288 # Determine where we are in the object tree relative to 'section'
289 # (where the static tool was defined).
290 if section == 'global':
291 section = "/"
292 section = section.rstrip(r"\/")
293 branch = request.path_info[len(section) + 1:]
294 branch = unquote(branch.lstrip(r"\/"))
296 # If branch is "", filename will end in a slash
297 filename = os.path.join(dir, branch)
298 if debug:
299 cherrypy.log('Checking file %r to fulfill %r' %
300 (filename, request.path_info), 'TOOLS.STATICDIR')
302 # There's a chance that the branch pulled from the URL might
303 # have ".." or similar uplevel attacks in it. Check that the final
304 # filename is a child of dir.
305 if not os.path.normpath(filename).startswith(os.path.normpath(dir)):
306 raise cherrypy.HTTPError(403) # Forbidden
308 handled = _attempt(filename, content_types)
309 if not handled:
310 # Check for an index file if a folder was requested.
311 if index:
312 handled = _attempt(os.path.join(filename, index), content_types)
313 if handled:
314 request.is_index = filename[-1] in (r"\/")
315 return handled
317 def staticfile(filename, root=None, match="", content_types=None, debug=False):
318 """Serve a static resource from the given (root +) filename.
320 match
321 If given, request.path_info will be searched for the given
322 regular expression before attempting to serve static content.
324 content_types
325 If given, it should be a Python dictionary of
326 {file-extension: content-type} pairs, where 'file-extension' is
327 a string (e.g. "gif") and 'content-type' is the value to write
328 out in the Content-Type response header (e.g. "image/gif").
331 request = cherrypy.serving.request
332 if request.method not in ('GET', 'HEAD'):
333 if debug:
334 cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICFILE')
335 return False
337 if match and not re.search(match, request.path_info):
338 if debug:
339 cherrypy.log('request.path_info %r does not match pattern %r' %
340 (request.path_info, match), 'TOOLS.STATICFILE')
341 return False
343 # If filename is relative, make absolute using "root".
344 if not os.path.isabs(filename):
345 if not root:
346 msg = "Static tool requires an absolute filename (got '%s')." % filename
347 if debug:
348 cherrypy.log(msg, 'TOOLS.STATICFILE')
349 raise ValueError(msg)
350 filename = os.path.join(root, filename)
352 return _attempt(filename, content_types, debug=debug)