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, 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, stop), 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_boundary 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' % (start, 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')." % filename 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)
364