Package cherrypy :: Package lib :: Module static
[hide private]
[frames] | no frames]

Source Code for Module cherrypy.lib.static

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