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

root/branches/cherrypy-2.1/cherrypy/lib/cptools.py

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

Fix to 2.1, 2.2, 3.0 for bugs in Range slicing and final boundary. Also made the output match Apache output (CRLFs).

Line 
1 """
2 Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
3 All rights reserved.
4
5 Redistribution and use in source and binary forms, with or without modification,
6 are permitted provided that the following conditions are met:
7
8     * Redistributions of source code must retain the above copyright notice,
9       this list of conditions and the following disclaimer.
10     * Redistributions in binary form must reproduce the above copyright notice,
11       this list of conditions and the following disclaimer in the documentation
12       and/or other materials provided with the distribution.
13     * Neither the name of the CherryPy Team nor the names of its contributors
14       may be used to endorse or promote products derived from this software
15       without specific prior written permission.
16
17 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
18 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
21 FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
24 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
25 OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 """
28
29 """
30 Tools which both the CherryPy framework and application developers invoke.
31 """
32
33 from BaseHTTPServer import BaseHTTPRequestHandler
34 responseCodes = BaseHTTPRequestHandler.responses.copy()
35
36 import inspect
37 import mimetools
38
39 import mimetypes
40 mimetypes.types_map['.dwg']='image/x-dwg'
41 mimetypes.types_map['.ico']='image/x-icon'
42
43 import os
44 import sys
45 import time
46
47
48 import cherrypy
49
50
51 def decorate(func, decorator):
52     """
53     Return the decorated func. This will automatically copy all
54     non-standard attributes (like exposed) to the newly decorated function.
55     """
56     newfunc = decorator(func)
57     for (k,v) in inspect.getmembers(func):
58         if not hasattr(newfunc, k):
59             setattr(newfunc, k, v)
60     return newfunc
61
62 def decorateAll(obj, decorator):
63     """
64     Recursively decorate all exposed functions of obj and all of its children,
65     grandchildren, etc. If you used to use aspects, you might want to look
66     into these. This function modifies obj; there is no return value.
67     """
68     obj_type = type(obj)
69     for (k,v) in inspect.getmembers(obj):
70         if hasattr(obj_type, k): # only deal with user-defined attributes
71             continue
72         if callable(v) and getattr(v, "exposed", False):
73             setattr(obj, k, decorate(v, decorator))
74         decorateAll(v, decorator)
75
76
77 class ExposeItems:
78     """
79     Utility class that exposes a getitem-aware object. It does not provide
80     index() or default() methods, and it does not expose the individual item
81     objects - just the list or dict that contains them. User-specific index()
82     and default() methods can be implemented by inheriting from this class.
83     
84     Use case:
85     
86     from cherrypy.lib.cptools import ExposeItems
87     ...
88     cherrypy.root.foo = ExposeItems(mylist)
89     cherrypy.root.bar = ExposeItems(mydict)
90     """
91     exposed = True
92     def __init__(self, items):
93         self.items = items
94     def __getattr__(self, key):
95         return self.items[key]
96
97
98 class PositionalParametersAware(object):
99     """
100     Utility class that restores positional parameters functionality that
101     was found in 2.0.0-beta.
102
103     Use case:
104
105     from cherrypy.lib import cptools
106     import cherrypy
107     class Root(cptools.PositionalParametersAware):
108         def something(self, name):
109             return "hello, " + name
110         something.exposed
111     cherrypy.root = Root()
112     cherrypy.server.start()
113
114     Now, fetch http://localhost:8080/something/name_is_here
115     """
116     def default( self, *args, **kwargs ):
117         # remap parameters to fix positional parameters
118         if len(args) == 0:
119             args = ("index",)
120         m = getattr(self, args[0], None)
121         if m and getattr(m, "exposed", False):
122             return getattr(self, args[0])(*args[1:], **kwargs)
123         else:
124             m = getattr(self, "index", None)
125             if m and getattr(m, "exposed", False):
126                 try:
127                     return self.index(*args, **kwargs)
128                 except TypeError:
129                     pass
130             raise cherrypy.NotFound()
131     default.exposed = True
132
133
134 weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
135 monthname = [None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
136                    'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
137
138 def HTTPDate(dt=None):
139     """Return the given time.struct_time as a string in RFC 1123 format.
140     
141     If no arguments are provided, the current time (as determined by
142     time.gmtime() is used).
143     
144     RFC 2616: "[Concerning RFC 1123, RFC 850, asctime date formats]...
145     HTTP/1.1 clients and servers that parse the date value MUST
146     accept all three formats (for compatibility with HTTP/1.0),
147     though they MUST only generate the RFC 1123 format for
148     representing HTTP-date values in header fields."
149     
150     RFC 1945 (HTTP/1.0) requires the same.
151     
152     """
153    
154     if dt is None:
155         dt = time.gmtime()
156    
157     year, month, day, hh, mm, ss, wd, y, z = dt
158     # Is "%a, %d %b %Y %H:%M:%S GMT" better or worse?
159     return ("%s, %02d %3s %4d %02d:%02d:%02d GMT" %
160             (weekdayname[wd], day, monthname[month], year, hh, mm, ss))
161
162
163 def getRanges(content_length):
164     """Return a list of (start, stop) indices from a Range header, or None.
165     
166     Each (start, stop) tuple will be composed of two ints, which are suitable
167     for use in a slicing operation. That is, the header "Range: bytes=3-6",
168     if applied against a Python string, is requesting resource[3:7]. This
169     function will return the list [(3, 7)].
170     """
171    
172     r = cherrypy.request.headerMap.get('Range')
173     if not r:
174         return None
175    
176     result = []
177     bytesunit, byteranges = r.split("=", 1)
178     for brange in byteranges.split(","):
179         start, stop = [x.strip() for x in brange.split("-", 1)]
180         if start:
181             if not stop:
182                 stop = content_length - 1
183             start, stop = map(int, (start, stop))
184             if start >= content_length:
185                 # From rfc 2616 sec 14.16:
186                 # "If the server receives a request (other than one
187                 # including an If-Range request-header field) with an
188                 # unsatisfiable Range request-header field (that is,
189                 # all of whose byte-range-spec values have a first-byte-pos
190                 # value greater than the current length of the selected
191                 # resource), it SHOULD return a response code of 416
192                 # (Requested range not satisfiable)."
193                 continue
194             if stop < start:
195                 # From rfc 2616 sec 14.16:
196                 # "If the server ignores a byte-range-spec because it
197                 # is syntactically invalid, the server SHOULD treat
198                 # the request as if the invalid Range header field
199                 # did not exist. (Normally, this means return a 200
200                 # response containing the full entity)."
201                 return None
202             result.append((start, stop + 1))
203         else:
204             if not stop:
205                 # See rfc quote above.
206                 return None
207             # Negative subscript (last N bytes)
208             result.append((content_length - int(stop), content_length))
209    
210     if result == []:
211         cherrypy.response.headerMap['Content-Range'] = "bytes */%s" % content_length
212         message = "Invalid Range (first-byte-pos greater than Content-Length)"
213         raise cherrypy.HTTPError(416, message)
214    
215     return result
216
217
218 def serveFile(path, contentType=None, disposition=None, name=None):
219     """Set status, headers, and body in order to serve the given file.
220     
221     The Content-Type header will be set to the contentType arg, if provided.
222     If not provided, the Content-Type will be guessed by its extension.
223     
224     If disposition is not None, the Content-Disposition header will be set
225     to "<disposition>; filename=<name>". If name is None, it will be set
226     to the basename of path. If disposition is None, no Content-Disposition
227     header will be written.
228     """
229    
230     response = cherrypy.response
231    
232     # If path is relative, make absolute using cherrypy.root's module.
233     # If there is no cherrypy.root, or it doesn't have a __module__
234     # attribute, then users should fix the issue by making path absolute.
235     # That is, CherryPy should not guess where the application root is
236     # any further than trying cherrypy.root.__module__, and it certainly
237     # should *not* use cwd (since CP may be invoked from a variety of
238     # paths). If using staticFilter, you can make your relative paths
239     # become absolute by supplying a value for "staticFilter.root".
240     if not os.path.isabs(path):
241         root = os.path.dirname(sys.modules[cherrypy.root.__module__].__file__)
242         path = os.path.join(root, path)
243    
244     try:
245         stat = os.stat(path)
246     except OSError:
247         if cherrypy.config.get('server.environment') == 'development':
248             cherrypy.log("    NOT FOUND file: %s" % path, "DEBUG")
249         raise cherrypy.NotFound()
250    
251     if contentType is None:
252         # Set content-type based on filename extension
253         ext = ""
254         i = path.rfind('.')
255         if i != -1:
256             ext = path[i:]
257         contentType = mimetypes.types_map.get(ext, "text/plain")
258     response.headerMap['Content-Type'] = contentType
259    
260     strModifTime = HTTPDate(time.gmtime(stat.st_mtime))
261     if cherrypy.request.headerMap.has_key('If-Modified-Since'):
262         # Check if if-modified-since date is the same as strModifTime
263         if cherrypy.request.headerMap['If-Modified-Since'] == strModifTime:
264             response.status = "304 Not Modified"
265             response.body = []
266             if getattr(cherrypy, "debug", None):
267                 cherrypy.log("    Found file (304 Not Modified): %s" % path, "DEBUG")
268             return []
269     response.headerMap['Last-Modified'] = strModifTime
270    
271     if disposition is not None:
272         if name is None:
273             name = os.path.basename(path)
274         cd = "%s; filename=%s" % (disposition, name)
275         response.headerMap["Content-Disposition"] = cd
276    
277     # Set Content-Length and use an iterable (file object)
278     #   this way CP won't load the whole file in memory
279     c_len = stat.st_size
280     bodyfile = open(path, 'rb')
281     if getattr(cherrypy, "debug", None):
282         cherrypy.log("    Found file: %s" % path, "DEBUG")
283    
284     # HTTP/1.0 didn't have Range/Accept-Ranges headers, or the 206 code
285     if cherrypy.response.version >= "1.1":
286         response.headerMap["Accept-Ranges"] = "bytes"
287         r = getRanges(c_len)
288         if r:
289             if len(r) == 1:
290                 # Return a single-part response.
291                 start, stop = r[0]
292                 r_len = stop - start
293                 response.status = "206 Partial Content"
294                 response.headerMap['Content-Range'] = ("bytes %s-%s/%s" %
295                                                        (start, stop - 1, c_len))
296                 response.headerMap['Content-Length'] = r_len
297                 bodyfile.seek(start)
298                 response.body = [bodyfile.read(r_len)]
299             else:
300                 # Return a multipart/byteranges response.
301                 response.status = "206 Partial Content"
302                 boundary = mimetools.choose_boundary()
303                 ct = "multipart/byteranges; boundary=%s" % boundary
304                 response.headerMap['Content-Type'] = ct
305 ##                del response.headerMap['Content-Length']
306                
307                 def fileRanges():
308                     for start, stop in r:
309                         yield "--" + boundary
310                         yield "\nContent-type: %s" % contentType
311                         yield ("\nContent-range: bytes %s-%s/%s\n\n"
312                                % (start, stop - 1, c_len))
313                         bodyfile.seek(start)
314                         yield bodyfile.read(stop - start)
315                         yield "\n"
316                     # Final boundary
317                     yield "--" + boundary + "--"
318                 response.body = fileRanges()
319         else:
320             response.headerMap['Content-Length'] = c_len
321             response.body = fileGenerator(bodyfile)
322     else:
323         response.headerMap['Content-Length'] = c_len
324         response.body = fileGenerator(bodyfile)
325     return response.body
326
327 def fileGenerator(input, chunkSize=65536):
328     """Yield the given input (a file object) in chunks (default 64k)."""
329     chunk = input.read(chunkSize)
330     while chunk:
331         yield chunk
332         chunk = input.read(chunkSize)
333     input.close()
334
335 def validStatus(status):
336     """Return legal HTTP status Code, Reason-phrase and Message.
337     
338     The status arg must be an int, or a str that begins with an int.
339     
340     If status is an int, or a str and  no reason-phrase is supplied,
341     a default reason-phrase will be provided.
342     """
343    
344     if not status:
345         status = 200
346    
347     status = str(status)
348     parts = status.split(" ", 1)
349     if len(parts) == 1:
350         # No reason supplied.
351         code, = parts
352         reason = None
353     else:
354         code, reason = parts
355         reason = reason.strip()
356    
357     try:
358         code = int(code)
359     except ValueError:
360         raise cherrypy.HTTPError(500, "Illegal response status from server (non-numeric).")
361    
362     if code < 100 or code > 599:
363         raise cherrypy.HTTPError(500, "Illegal response status from server (out of range).")
364    
365     if code not in responseCodes:
366         # code is unknown but not illegal
367         defaultReason, message = "", ""
368     else:
369         defaultReason, message = responseCodes[code]
370    
371     if reason is None:
372         reason = defaultReason
373    
374     return code, reason, message
Note: See TracBrowser for help on using the browser.

Hosted by WebFaction

Log in as guest/cpguest to create tickets