Download Install Tutorial Docs FAQ Tools WikiLicense Team IRC Planet Involvement Shop Book

root/tags/cherrypy-3.0.0/cherrypy/lib/static.py

Revision 1402 (checked in by fumanchu, 2 years ago)

Trunk fix for #577 (GzipFilter doesn't force an update of the Content-Length header). All code which could change the length of response.body should delete the Content-Length header (if already set).

  • Property svn:eol-style set to native
Line 
1 import mimetools
2 import mimetypes
3 mimetypes.init()
4 mimetypes.types_map['.dwg']='image/x-dwg'
5 mimetypes.types_map['.ico']='image/x-icon'
6
7 import os
8 import re
9 import stat
10 import time
11 import urllib
12
13 import cherrypy
14 from cherrypy.lib import cptools, http
15
16
17 def serve_file(path, content_type=None, disposition=None, name=None):
18     """Set status, headers, and body in order to serve the given file.
19     
20     The Content-Type header will be set to the content_type arg, if provided.
21     If not provided, the Content-Type will be guessed by its extension.
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                 r_len = stop - start
87                 response.status = "206 Partial Content"
88                 response.headers['Content-Range'] = ("bytes %s-%s/%s" %
89                                                        (start, stop - 1, c_len))
90                 response.headers['Content-Length'] = r_len
91                 bodyfile.seek(start)
92                 response.body = bodyfile.read(r_len)
93             else:
94                 # Return a multipart/byteranges response.
95                 response.status = "206 Partial Content"
96                 boundary = mimetools.choose_boundary()
97                 ct = "multipart/byteranges; boundary=%s" % boundary
98                 response.headers['Content-Type'] = ct
99                 if response.headers.has_key("Content-Length"):
100                     # Delete Content-Length header so finalize() recalcs it.
101                     del response.headers["Content-Length"]
102                
103                 def file_ranges():
104                     # Apache compatibility:
105                     yield "\r\n"
106                    
107                     for start, stop in r:
108                         yield "--" + boundary
109                         yield "\r\nContent-type: %s" % content_type
110                         yield ("\r\nContent-range: bytes %s-%s/%s\r\n\r\n"
111                                % (start, stop - 1, c_len))
112                         bodyfile.seek(start)
113                         yield bodyfile.read(stop - start)
114                         yield "\r\n"
115                     # Final boundary
116                     yield "--" + boundary + "--"
117                    
118                     # Apache compatibility:
119                     yield "\r\n"
120                 response.body = file_ranges()
121         else:
122             response.headers['Content-Length'] = c_len
123             response.body = bodyfile
124     else:
125         response.headers['Content-Length'] = c_len
126         response.body = bodyfile
127     return response.body
128
129 def serve_download(path, name=None):
130     """Serve 'path' as an application/x-download attachment."""
131     # This is such a common idiom I felt it deserved its own wrapper.
132     return serve_file(path, "application/x-download", "attachment", name)
133
134
135 def _attempt(filename, content_types):
136     try:
137         # you can set the content types for a
138         # complete directory per extension
139         content_type = None
140         if content_types:
141             r, ext = os.path.splitext(filename)
142             content_type = content_types.get(ext[1:], None)
143         serve_file(filename, content_type=content_type)
144         return True
145     except cherrypy.NotFound:
146         # If we didn't find the static file, continue handling the
147         # request. We might find a dynamic handler instead.
148         return False
149
150 def staticdir(section, dir, root="", match="", content_types=None, index=""):
151     """Serve a static resource from the given (root +) dir."""
152     if match and not re.search(match, cherrypy.request.path_info):
153         return False
154    
155     # If dir is relative, make absolute using "root".
156     if not os.path.isabs(dir):
157         if not root:
158             msg = "Static dir requires an absolute dir (or root)."
159             raise ValueError(msg)
160         dir = os.path.join(root, dir)
161    
162     # Determine where we are in the object tree relative to 'section'
163     # (where the static tool was defined).
164     if section == 'global':
165         section = "/"
166     section = section.rstrip(r"\/")
167     branch = cherrypy.request.path_info[len(section) + 1:]
168     branch = urllib.unquote(branch.lstrip(r"\/"))
169    
170     # If branch is "", filename will end in a slash
171     filename = os.path.join(dir, branch)
172    
173     # There's a chance that the branch pulled from the URL might
174     # have ".." or similar uplevel attacks in it. Check that the final
175     # filename is a child of dir.
176     if not os.path.normpath(filename).startswith(os.path.normpath(dir)):
177         raise cherrypy.HTTPError(403) # Forbidden
178    
179     handled = _attempt(filename, content_types)
180     if not handled:
181         # Check for an index file if a folder was requested.
182         if index and filename[-1] in (r"\/"):
183             handled = _attempt(os.path.join(filename, index), content_types)
184     return handled
185
186 def staticfile(filename, root=None, match="", content_types=None):
187     """Serve a static resource from the given (root +) filename."""
188     if match and not re.search(match, cherrypy.request.path_info):
189         return False
190    
191     # If filename is relative, make absolute using "root".
192     if not os.path.isabs(filename):
193         if not root:
194             msg = "Static tool requires an absolute filename (got '%s')." % filename
195             raise ValueError(msg)
196         filename = os.path.join(root, filename)
197    
198     return _attempt(filename, content_types)
Note: See TracBrowser for help on using the browser.

Hosted by WebFaction

Log in as guest/cpguest to create tickets