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

Source Code for Module cherrypy.lib.static

  1  import mimetypes 
  2  mimetypes.init() 
  3  mimetypes.types_map['.dwg']='image/x-dwg' 
  4  mimetypes.types_map['.ico']='image/x-icon' 
  5   
  6  import os 
  7  import re 
  8  import stat 
  9  import time 
 10  import urllib 
 11   
 12  import cherrypy 
 13  from cherrypy.lib import cptools, http, file_generator_limited 
 14   
 15   
16 -def serve_file(path, content_type=None, disposition=None, name=None):
17 """Set status, headers, and body in order to serve the given file. 18 19 The Content-Type header will be set to the content_type arg, if provided. 20 If not provided, the Content-Type will be guessed by the file extension 21 of the 'path' argument. 22 23 If disposition is not None, the Content-Disposition header will be set 24 to "<disposition>; filename=<name>". If name is None, it will be set 25 to the basename of path. If disposition is None, no Content-Disposition 26 header will be written. 27 """ 28 29 response = cherrypy.response 30 31 # If path is relative, users should fix it by making path absolute. 32 # That is, CherryPy should not guess where the application root is. 33 # It certainly should *not* use cwd (since CP may be invoked from a 34 # variety of paths). If using tools.static, you can make your relative 35 # paths become absolute by supplying a value for "tools.static.root". 36 if not os.path.isabs(path): 37 raise ValueError("'%s' is not an absolute path." % path) 38 39 try: 40 st = os.stat(path) 41 except OSError: 42 raise cherrypy.NotFound() 43 44 # Check if path is a directory. 45 if stat.S_ISDIR(st.st_mode): 46 # Let the caller deal with it as they like. 47 raise cherrypy.NotFound() 48 49 # Set the Last-Modified response header, so that 50 # modified-since validation code can work. 51 response.headers['Last-Modified'] = http.HTTPDate(st.st_mtime) 52 cptools.validate_since() 53 54 if content_type is None: 55 # Set content-type based on filename extension 56 ext = "" 57 i = path.rfind('.') 58 if i != -1: 59 ext = path[i:].lower() 60 content_type = mimetypes.types_map.get(ext, "text/plain") 61 response.headers['Content-Type'] = content_type 62 63 if disposition is not None: 64 if name is None: 65 name = os.path.basename(path) 66 cd = '%s; filename="%s"' % (disposition, name) 67 response.headers["Content-Disposition"] = cd 68 69 # Set Content-Length and use an iterable (file object) 70 # this way CP won't load the whole file in memory 71 c_len = st.st_size 72 bodyfile = open(path, 'rb') 73 74 # HTTP/1.0 didn't have Range/Accept-Ranges headers, or the 206 code 75 if cherrypy.request.protocol >= (1, 1): 76 response.headers["Accept-Ranges"] = "bytes" 77 r = http.get_ranges(cherrypy.request.headers.get('Range'), c_len) 78 if r == []: 79 response.headers['Content-Range'] = "bytes */%s" % c_len 80 message = "Invalid Range (first-byte-pos greater than Content-Length)" 81 raise cherrypy.HTTPError(416, message) 82 if r: 83 if len(r) == 1: 84 # Return a single-part response. 85 start, stop = r[0] 86 if stop > c_len: 87 stop = c_len 88 r_len = stop - start 89 response.status = "206 Partial Content" 90 response.headers['Content-Range'] = ("bytes %s-%s/%s" % 91 (start, stop - 1, c_len)) 92 response.headers['Content-Length'] = r_len 93 bodyfile.seek(start) 94 response.body = file_generator_limited(bodyfile, r_len) 95 else: 96 # Return a multipart/byteranges response. 97 response.status = "206 Partial Content" 98 import mimetools 99 boundary = mimetools.choose_boundary() 100 ct = "multipart/byteranges; boundary=%s" % boundary 101 response.headers['Content-Type'] = ct 102 if response.headers.has_key("Content-Length"): 103 # Delete Content-Length header so finalize() recalcs it. 104 del response.headers["Content-Length"] 105 106 def file_ranges(): 107 # Apache compatibility: 108 yield "\r\n" 109 110 for start, stop in r: 111 yield "--" + boundary 112 yield "\r\nContent-type: %s" % content_type 113 yield ("\r\nContent-range: bytes %s-%s/%s\r\n\r\n" 114 % (start, stop - 1, c_len)) 115 bodyfile.seek(start) 116 for chunk in file_generator_limited(bodyfile, stop-start): 117 yield chunk 118 yield "\r\n" 119 # Final boundary 120 yield "--" + boundary + "--" 121 122 # Apache compatibility: 123 yield "\r\n"
124 response.body = file_ranges() 125 else: 126 response.headers['Content-Length'] = c_len 127 response.body = bodyfile 128 else: 129 response.headers['Content-Length'] = c_len 130 response.body = bodyfile 131 return response.body 132
133 -def serve_download(path, name=None):
134 """Serve 'path' as an application/x-download attachment.""" 135 # This is such a common idiom I felt it deserved its own wrapper. 136 return serve_file(path, "application/x-download", "attachment", name)
137 138
139 -def _attempt(filename, content_types):
140 try: 141 # you can set the content types for a 142 # complete directory per extension 143 content_type = None 144 if content_types: 145 r, ext = os.path.splitext(filename) 146 content_type = content_types.get(ext[1:], None) 147 serve_file(filename, content_type=content_type) 148 return True 149 except cherrypy.NotFound: 150 # If we didn't find the static file, continue handling the 151 # request. We might find a dynamic handler instead. 152 return False
153
154 -def staticdir(section, dir, root="", match="", content_types=None, index=""):
155 """Serve a static resource from the given (root +) dir. 156 157 If 'match' is given, request.path_info will be searched for the given 158 regular expression before attempting to serve static content. 159 160 If content_types is given, it should be a Python dictionary of 161 {file-extension: content-type} pairs, where 'file-extension' is 162 a string (e.g. "gif") and 'content-type' is the value to write 163 out in the Content-Type response header (e.g. "image/gif"). 164 165 If 'index' is provided, it should be the (relative) name of a file to 166 serve for directory requests. For example, if the dir argument is 167 '/home/me', the Request-URI is 'myapp', and the index arg is 168 'index.html', the file '/home/me/myapp/index.html' will be sought. 169 """ 170 if cherrypy.request.method not in ('GET', 'HEAD'): 171 return False 172 173 if match and not re.search(match, cherrypy.request.path_info): 174 return False 175 176 # Allow the use of '~' to refer to a user's home directory. 177 dir = os.path.expanduser(dir) 178 179 # If dir is relative, make absolute using "root". 180 if not os.path.isabs(dir): 181 if not root: 182 msg = "Static dir requires an absolute dir (or root)." 183 raise ValueError(msg) 184 dir = os.path.join(root, dir) 185 186 # Determine where we are in the object tree relative to 'section' 187 # (where the static tool was defined). 188 if section == 'global': 189 section = "/" 190 section = section.rstrip(r"\/") 191 branch = cherrypy.request.path_info[len(section) + 1:] 192 branch = urllib.unquote(branch.lstrip(r"\/")) 193 194 # If branch is "", filename will end in a slash 195 filename = os.path.join(dir, branch) 196 197 # There's a chance that the branch pulled from the URL might 198 # have ".." or similar uplevel attacks in it. Check that the final 199 # filename is a child of dir. 200 if not os.path.normpath(filename).startswith(os.path.normpath(dir)): 201 raise cherrypy.HTTPError(403) # Forbidden 202 203 handled = _attempt(filename, content_types) 204 if not handled: 205 # Check for an index file if a folder was requested. 206 if index: 207 handled = _attempt(os.path.join(filename, index), content_types) 208 if handled: 209 cherrypy.request.is_index = filename[-1] in (r"\/") 210 return handled
211
212 -def staticfile(filename, root=None, match="", content_types=None):
213 """Serve a static resource from the given (root +) filename. 214 215 If 'match' is given, request.path_info will be searched for the given 216 regular expression before attempting to serve static content. 217 218 If content_types is given, it should be a Python dictionary of 219 {file-extension: content-type} pairs, where 'file-extension' is 220 a string (e.g. "gif") and 'content-type' is the value to write 221 out in the Content-Type response header (e.g. "image/gif"). 222 """ 223 if cherrypy.request.method not in ('GET', 'HEAD'): 224 return False 225 226 if match and not re.search(match, cherrypy.request.path_info): 227 return False 228 229 # If filename is relative, make absolute using "root". 230 if not os.path.isabs(filename): 231 if not root: 232 msg = "Static tool requires an absolute filename (got '%s')." % filename 233 raise ValueError(msg) 234 filename = os.path.join(root, filename) 235 236 return _attempt(filename, content_types)
237