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