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

root/trunk/cherrypy/lib/static.py

Revision 1975 (checked in by fumanchu, 3 months ago)

Docstring updates.

  • Property svn:eol-style set to native
Line 
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
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                 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                 import mimetools
97                 boundary = mimetools.choose_boundary()
98                 ct = "multipart/byteranges; boundary=%s" % boundary
99                 response.headers['Content-Type'] = ct
100                 if response.headers.has_key("Content-Length"):
101                     # Delete Content-Length header so finalize() recalcs it.
102                     del response.headers["Content-Length"]
103                
104                 def file_ranges():
105                     # Apache compatibility:
106                     yield "\r\n"
107                    
108                     for start, stop in r:
109                         yield "--" + boundary
110                         yield "\r\nContent-type: %s" % content_type
111                         yield ("\r\nContent-range: bytes %s-%s/%s\r\n\r\n"
112                                % (start, stop - 1, c_len))
113                         bodyfile.seek(start)
114                         yield bodyfile.read(stop - start)
115                         yield "\r\n"
116                     # Final boundary
117                     yield "--" + boundary + "--"
118                    
119                     # Apache compatibility:
120                     yield "\r\n"
121                 response.body = file_ranges()
122         else:
123             response.headers['Content-Length'] = c_len
124             response.body = bodyfile
125     else:
126         response.headers['Content-Length'] = c_len
127         response.body = bodyfile
128     return response.body
129
130 def serve_download(path, name=None):
131     """Serve 'path' as an application/x-download attachment."""
132     # This is such a common idiom I felt it deserved its own wrapper.
133     return serve_file(path, "application/x-download", "attachment", name)
134
135
136 def _attempt(filename, content_types):
137     try:
138         # you can set the content types for a
139         # complete directory per extension
140         content_type = None
141         if content_types:
142             r, ext = os.path.splitext(filename)
143             content_type = content_types.get(ext[1:], None)
144         serve_file(filename, content_type=content_type)
145         return True
146     except cherrypy.NotFound:
147         # If we didn't find the static file, continue handling the
148         # request. We might find a dynamic handler instead.
149         return False
150
151 def staticdir(section, dir, root="", match="", content_types=None, index=""):
152     """Serve a static resource from the given (root +) dir.
153     
154     If 'match' is given, request.path_info will be searched for the given
155     regular expression before attempting to serve static content.
156     
157     If content_types is given, it should be a Python dictionary of
158     {file-extension: content-type} pairs, where 'file-extension' is
159     a string (e.g. "gif") and 'content-type' is the value to write
160     out in the Content-Type response header (e.g. "image/gif").
161     
162     If 'index' is provided, it should be the (relative) name of a file to
163     serve for directory requests. For example, if the dir argument is
164     '/home/me', the Request-URI is 'myapp', and the index arg is
165     'index.html', the file '/home/me/myapp/index.html' will be sought.
166     """
167     if match and not re.search(match, cherrypy.request.path_info):
168         return False
169    
170     # Allow the use of '~' to refer to a user's home directory.
171     dir = os.path.expanduser(dir)
172
173     # If dir is relative, make absolute using "root".
174     if not os.path.isabs(dir):
175         if not root:
176             msg = "Static dir requires an absolute dir (or root)."
177             raise ValueError(msg)
178         dir = os.path.join(root, dir)
179    
180     # Determine where we are in the object tree relative to 'section'
181     # (where the static tool was defined).
182     if section == 'global':
183         section = "/"
184     section = section.rstrip(r"\/")
185     branch = cherrypy.request.path_info[len(section) + 1:]
186     branch = urllib.unquote(branch.lstrip(r"\/"))
187    
188     # If branch is "", filename will end in a slash
189     filename = os.path.join(dir, branch)
190    
191     # There's a chance that the branch pulled from the URL might
192     # have ".." or similar uplevel attacks in it. Check that the final
193     # filename is a child of dir.
194     if not os.path.normpath(filename).startswith(os.path.normpath(dir)):
195         raise cherrypy.HTTPError(403) # Forbidden
196    
197     handled = _attempt(filename, content_types)
198     if not handled:
199         # Check for an index file if a folder was requested.
200         if index:
201             handled = _attempt(os.path.join(filename, index), content_types)
202             if handled:
203                 cherrypy.request.is_index = filename[-1] in (r"\/")
204     return handled
205
206 def staticfile(filename, root=None, match="", content_types=None):
207     """Serve a static resource from the given (root +) filename.
208     
209     If 'match' is given, request.path_info will be searched for the given
210     regular expression before attempting to serve static content.
211     
212     If content_types is given, it should be a Python dictionary of
213     {file-extension: content-type} pairs, where 'file-extension' is
214     a string (e.g. "gif") and 'content-type' is the value to write
215     out in the Content-Type response header (e.g. "image/gif").
216     """
217     if match and not re.search(match, cherrypy.request.path_info):
218         return False
219    
220     # If filename is relative, make absolute using "root".
221     if not os.path.isabs(filename):
222         if not root:
223             msg = "Static tool requires an absolute filename (got '%s')." % filename
224             raise ValueError(msg)
225         filename = os.path.join(root, filename)
226    
227     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