~ubuntu-branches/debian/jessie/cherrypy3/jessie

« back to all changes in this revision

Viewing changes to cherrypy/lib/static.py

  • Committer: Package Import Robot
  • Author(s): Gustavo Noronha Silva, JCF Ploemen, Stéphane Graber, Gustavo Noronha
  • Date: 2012-01-06 10:13:27 UTC
  • mfrom: (1.1.4) (7.1.2 sid)
  • Revision ID: package-import@ubuntu.com-20120106101327-smxnhguqs14ubl7e
Tags: 3.2.2-1
[ JCF Ploemen ]
* New upstream release (Closes: #571196).
* Bumped Standards-Version to 3.8.4 (no changes needed).
* Removing patch 02: no longer needed, incorporated upstream.
* Updating patch 00 to match release.
* Install cherryd man page via debian/manpages.
* debian/copyright:
  + Added notice for cherrypy/lib/httpauth.py.
  + Fixed years.
* debian/watch:
  + Don't hit on the -py3 release by blocking '-' from the version.
  + Mangle upstream version, inserting a tilde for beta/rc.

[ Stéphane Graber <stgraber@ubuntu.com> ]
 * Convert from python-support to dh_python2 (#654375)
  - debian/pyversions: Removed (no longer needed)
  - debian/rules
   + Replace call to dh_pysupport by dh_python2
   + Add --with=python2 to all dh calls
  - debian/control
   + Drop build-depends on python-support
   + Bump build-depends on python-all to >= 2.6.6-3~
   + Replace XS-Python-Version by X-Python-Version
   + Remove XB-Python-Version from binary package

[ Gustavo Noronha ]
* debian/control, debian/rules, debian/manpages:
 - use help2man to generate a manpage for cherryd at build time, since
  one is no longer shipped along with the source code
* debian/control:
- add python-nose to Build-Depends, since it's used during the
  documentation build for cross-reference generation

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
try:
 
2
    from io import UnsupportedOperation
 
3
except ImportError:
 
4
    UnsupportedOperation = object()
 
5
import logging
1
6
import mimetypes
2
7
mimetypes.init()
3
8
mimetypes.types_map['.dwg']='image/x-dwg'
4
9
mimetypes.types_map['.ico']='image/x-icon'
 
10
mimetypes.types_map['.bz2']='application/x-bzip2'
 
11
mimetypes.types_map['.gz']='application/x-gzip'
5
12
 
6
13
import os
7
14
import re
8
15
import stat
9
16
import time
10
 
import urllib
11
17
 
12
18
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.
 
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.
18
25
    
19
26
    The Content-Type header will be set to the content_type arg, if provided.
20
27
    If not provided, the Content-Type will be guessed by the file extension
26
33
    header will be written.
27
34
    """
28
35
    
29
 
    response = cherrypy.response
 
36
    response = cherrypy.serving.response
30
37
    
31
38
    # If path is relative, users should fix it by making path absolute.
32
39
    # That is, CherryPy should not guess where the application root is.
33
40
    # 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".
 
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".
36
43
    if not os.path.isabs(path):
37
 
        raise ValueError("'%s' is not an absolute path." % path)
 
44
        msg = "'%s' is not an absolute path." % path
 
45
        if debug:
 
46
            cherrypy.log(msg, 'TOOLS.STATICFILE')
 
47
        raise ValueError(msg)
38
48
    
39
49
    try:
40
50
        st = os.stat(path)
41
51
    except OSError:
 
52
        if debug:
 
53
            cherrypy.log('os.stat(%r) failed' % path, 'TOOLS.STATIC')
42
54
        raise cherrypy.NotFound()
43
55
    
44
56
    # Check if path is a directory.
45
57
    if stat.S_ISDIR(st.st_mode):
46
58
        # Let the caller deal with it as they like.
 
59
        if debug:
 
60
            cherrypy.log('%r is a directory' % path, 'TOOLS.STATIC')
47
61
        raise cherrypy.NotFound()
48
62
    
49
63
    # Set the Last-Modified response header, so that
50
64
    # modified-since validation code can work.
51
 
    response.headers['Last-Modified'] = http.HTTPDate(st.st_mtime)
 
65
    response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime)
52
66
    cptools.validate_since()
53
67
    
54
68
    if content_type is None:
57
71
        i = path.rfind('.')
58
72
        if i != -1:
59
73
            ext = path[i:].lower()
60
 
        content_type = mimetypes.types_map.get(ext, "text/plain")
61
 
    response.headers['Content-Type'] = content_type
 
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')
62
79
    
 
80
    cd = None
63
81
    if disposition is not None:
64
82
        if name is None:
65
83
            name = os.path.basename(path)
66
84
        cd = '%s; filename="%s"' % (disposition, name)
67
85
        response.headers["Content-Disposition"] = cd
 
86
    if debug:
 
87
        cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
68
88
    
69
89
    # Set Content-Length and use an iterable (file object)
70
90
    #   this way CP won't load the whole file in memory
71
 
    c_len = st.st_size
72
 
    bodyfile = open(path, 'rb')
 
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
73
151
    
74
152
    # HTTP/1.0 didn't have Range/Accept-Ranges headers, or the 206 code
75
 
    if cherrypy.request.protocol >= (1, 1):
 
153
    request = cherrypy.serving.request
 
154
    if request.protocol >= (1, 1):
76
155
        response.headers["Accept-Ranges"] = "bytes"
77
 
        r = http.get_ranges(cherrypy.request.headers.get('Range'), c_len)
 
156
        r = httputil.get_ranges(request.headers.get('Range'), content_length)
78
157
        if r == []:
79
 
            response.headers['Content-Range'] = "bytes */%s" % c_len
 
158
            response.headers['Content-Range'] = "bytes */%s" % content_length
80
159
            message = "Invalid Range (first-byte-pos greater than Content-Length)"
 
160
            if debug:
 
161
                cherrypy.log(message, 'TOOLS.STATIC')
81
162
            raise cherrypy.HTTPError(416, message)
 
163
        
82
164
        if r:
83
165
            if len(r) == 1:
84
166
                # Return a single-part response.
85
167
                start, stop = r[0]
86
 
                if stop > c_len:
87
 
                    stop = c_len
 
168
                if stop > content_length:
 
169
                    stop = content_length
88
170
                r_len = stop - start
 
171
                if debug:
 
172
                    cherrypy.log('Single part; start: %r, stop: %r' % (start, stop),
 
173
                                 'TOOLS.STATIC')
89
174
                response.status = "206 Partial Content"
90
 
                response.headers['Content-Range'] = ("bytes %s-%s/%s" %
91
 
                                                       (start, stop - 1, c_len))
 
175
                response.headers['Content-Range'] = (
 
176
                    "bytes %s-%s/%s" % (start, stop - 1, content_length))
92
177
                response.headers['Content-Length'] = r_len
93
 
                bodyfile.seek(start)
94
 
                response.body = file_generator_limited(bodyfile, r_len)
 
178
                fileobj.seek(start)
 
179
                response.body = file_generator_limited(fileobj, r_len)
95
180
            else:
96
181
                # Return a multipart/byteranges response.
97
182
                response.status = "206 Partial Content"
98
 
                import mimetools
99
 
                boundary = mimetools.choose_boundary()
 
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()
100
190
                ct = "multipart/byteranges; boundary=%s" % boundary
101
191
                response.headers['Content-Type'] = ct
102
 
                if response.headers.has_key("Content-Length"):
 
192
                if "Content-Length" in response.headers:
103
193
                    # Delete Content-Length header so finalize() recalcs it.
104
194
                    del response.headers["Content-Length"]
105
195
                
106
196
                def file_ranges():
107
197
                    # Apache compatibility:
108
 
                    yield "\r\n"
 
198
                    yield ntob("\r\n")
109
199
                    
110
200
                    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):
 
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):
117
210
                            yield chunk
118
 
                        yield "\r\n"
 
211
                        yield ntob("\r\n")
119
212
                    # Final boundary
120
 
                    yield "--" + boundary + "--"
 
213
                    yield ntob("--" + boundary + "--", 'ascii')
121
214
                    
122
215
                    # Apache compatibility:
123
 
                    yield "\r\n"
 
216
                    yield ntob("\r\n")
124
217
                response.body = file_ranges()
 
218
            return response.body
125
219
        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
 
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
131
227
    return response.body
132
228
 
133
229
def serve_download(path, name=None):
136
232
    return serve_file(path, "application/x-download", "attachment", name)
137
233
 
138
234
 
139
 
def _attempt(filename, content_types):
 
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')
140
239
    try:
141
240
        # you can set the content types for a
142
241
        # complete directory per extension
144
243
        if content_types:
145
244
            r, ext = os.path.splitext(filename)
146
245
            content_type = content_types.get(ext[1:], None)
147
 
        serve_file(filename, content_type=content_type)
 
246
        serve_file(filename, content_type=content_type, debug=debug)
148
247
        return True
149
248
    except cherrypy.NotFound:
150
249
        # If we didn't find the static file, continue handling the
151
250
        # request. We might find a dynamic handler instead.
 
251
        if debug:
 
252
            cherrypy.log('NotFound', 'TOOLS.STATICFILE')
152
253
        return False
153
254
 
154
 
def staticdir(section, dir, root="", match="", content_types=None, index=""):
 
255
def staticdir(section, dir, root="", match="", content_types=None, index="",
 
256
              debug=False):
155
257
    """Serve a static resource from the given (root +) dir.
156
258
    
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.
 
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.
169
274
    """
170
 
    if cherrypy.request.method not in ('GET', 'HEAD'):
 
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')
171
279
        return False
172
280
    
173
 
    if match and not re.search(match, cherrypy.request.path_info):
 
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')
174
285
        return False
175
286
    
176
287
    # Allow the use of '~' to refer to a user's home directory.
180
291
    if not os.path.isabs(dir):
181
292
        if not root:
182
293
            msg = "Static dir requires an absolute dir (or root)."
 
294
            if debug:
 
295
                cherrypy.log(msg, 'TOOLS.STATICDIR')
183
296
            raise ValueError(msg)
184
297
        dir = os.path.join(root, dir)
185
298
    
188
301
    if section == 'global':
189
302
        section = "/"
190
303
    section = section.rstrip(r"\/")
191
 
    branch = cherrypy.request.path_info[len(section) + 1:]
192
 
    branch = urllib.unquote(branch.lstrip(r"\/"))
 
304
    branch = request.path_info[len(section) + 1:]
 
305
    branch = unquote(branch.lstrip(r"\/"))
193
306
    
194
307
    # If branch is "", filename will end in a slash
195
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')
196
312
    
197
313
    # There's a chance that the branch pulled from the URL might
198
314
    # have ".." or similar uplevel attacks in it. Check that the final
206
322
        if index:
207
323
            handled = _attempt(os.path.join(filename, index), content_types)
208
324
            if handled:
209
 
                cherrypy.request.is_index = filename[-1] in (r"\/")
 
325
                request.is_index = filename[-1] in (r"\/")
210
326
    return handled
211
327
 
212
 
def staticfile(filename, root=None, match="", content_types=None):
 
328
def staticfile(filename, root=None, match="", content_types=None, debug=False):
213
329
    """Serve a static resource from the given (root +) filename.
214
330
    
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").
 
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
    
222
341
    """
223
 
    if cherrypy.request.method not in ('GET', 'HEAD'):
 
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')
224
346
        return False
225
347
    
226
 
    if match and not re.search(match, cherrypy.request.path_info):
 
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')
227
352
        return False
228
353
    
229
354
    # If filename is relative, make absolute using "root".
230
355
    if not os.path.isabs(filename):
231
356
        if not root:
232
357
            msg = "Static tool requires an absolute filename (got '%s')." % filename
 
358
            if debug:
 
359
                cherrypy.log(msg, 'TOOLS.STATICFILE')
233
360
            raise ValueError(msg)
234
361
        filename = os.path.join(root, filename)
235
362
    
236
 
    return _attempt(filename, content_types)
 
363
    return _attempt(filename, content_types, debug=debug)