2
from io import UnsupportedOperation
4
UnsupportedOperation = object()
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'
13
from cherrypy.lib import cptools, http, file_generator_limited
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
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.
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.
29
response = cherrypy.response
36
response = cherrypy.serving.response
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
46
cherrypy.log(msg, 'TOOLS.STATICFILE')
53
cherrypy.log('os.stat(%r) failed' % path, 'TOOLS.STATIC')
42
54
raise cherrypy.NotFound()
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.
60
cherrypy.log('%r is a directory' % path, 'TOOLS.STATIC')
47
61
raise cherrypy.NotFound()
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()
54
68
if content_type is None:
57
71
i = path.rfind('.')
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
78
cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC')
63
81
if disposition is not None:
65
83
name = os.path.basename(path)
66
84
cd = '%s; filename="%s"' % (disposition, name)
67
85
response.headers["Content-Disposition"] = cd
87
cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
69
89
# Set Content-Length and use an iterable (file object)
70
90
# this way CP won't load the whole file in memory
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)
95
def serve_fileobj(fileobj, content_type=None, disposition=None, name=None,
97
"""Set status, headers, and body in order to serve the given file object.
99
The Content-Type header will be set to the content_type arg, if provided.
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
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
114
response = cherrypy.serving.response
117
st = os.fstat(fileobj.fileno())
118
except AttributeError:
120
cherrypy.log('os has no fstat attribute', 'TOOLS.STATIC')
121
content_length = None
122
except UnsupportedOperation:
123
content_length = None
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
131
if content_type is not None:
132
response.headers['Content-Type'] = content_type
134
cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC')
137
if disposition is not None:
141
cd = '%s; filename="%s"' % (disposition, name)
142
response.headers["Content-Disposition"] = cd
144
cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
146
return _serve_fileobj(fileobj, content_type, content_length, debug=debug)
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
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)
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)"
161
cherrypy.log(message, 'TOOLS.STATIC')
81
162
raise cherrypy.HTTPError(416, message)
84
166
# Return a single-part response.
85
167
start, stop = r[0]
168
if stop > content_length:
169
stop = content_length
88
170
r_len = stop - start
172
cherrypy.log('Single part; start: %r, stop: %r' % (start, stop),
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
94
response.body = file_generator_limited(bodyfile, r_len)
179
response.body = file_generator_limited(fileobj, r_len)
96
181
# Return a multipart/byteranges response.
97
182
response.status = "206 Partial Content"
99
boundary = mimetools.choose_boundary()
185
from email.generator import _make_boundary as choose_boundary
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"]
106
196
def file_ranges():
107
197
# Apache compatibility:
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))
116
for chunk in file_generator_limited(bodyfile, stop-start):
202
cherrypy.log('Multipart; start: %r, stop: %r' % (start, stop),
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')
209
for chunk in file_generator_limited(fileobj, stop-start):
120
yield "--" + boundary + "--"
213
yield ntob("--" + boundary + "--", 'ascii')
122
215
# Apache compatibility:
124
217
response.body = file_ranges()
126
response.headers['Content-Length'] = c_len
127
response.body = bodyfile
129
response.headers['Content-Length'] = c_len
130
response.body = bodyfile
221
cherrypy.log('No byteranges requested', 'TOOLS.STATIC')
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
133
229
def serve_download(path, name=None):
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)
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.
252
cherrypy.log('NotFound', 'TOOLS.STATICFILE')
154
def staticdir(section, dir, root="", match="", content_types=None, index=""):
255
def staticdir(section, dir, root="", match="", content_types=None, index="",
155
257
"""Serve a static resource from the given (root +) dir.
157
If 'match' is given, request.path_info will be searched for the given
158
regular expression before attempting to serve static content.
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").
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.
260
If given, request.path_info will be searched for the given
261
regular expression before attempting to serve static content.
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").
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.
170
if cherrypy.request.method not in ('GET', 'HEAD'):
275
request = cherrypy.serving.request
276
if request.method not in ('GET', 'HEAD'):
278
cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICDIR')
173
if match and not re.search(match, cherrypy.request.path_info):
281
if match and not re.search(match, request.path_info):
283
cherrypy.log('request.path_info %r does not match pattern %r' %
284
(request.path_info, match), 'TOOLS.STATICDIR')
176
287
# Allow the use of '~' to refer to a user's home directory.
207
323
handled = _attempt(os.path.join(filename, index), content_types)
209
cherrypy.request.is_index = filename[-1] in (r"\/")
325
request.is_index = filename[-1] in (r"\/")
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.
215
If 'match' is given, request.path_info will be searched for the given
216
regular expression before attempting to serve static content.
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").
332
If given, request.path_info will be searched for the given
333
regular expression before attempting to serve static content.
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").
223
if cherrypy.request.method not in ('GET', 'HEAD'):
342
request = cherrypy.serving.request
343
if request.method not in ('GET', 'HEAD'):
345
cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICFILE')
226
if match and not re.search(match, cherrypy.request.path_info):
348
if match and not re.search(match, request.path_info):
350
cherrypy.log('request.path_info %r does not match pattern %r' %
351
(request.path_info, match), 'TOOLS.STATICFILE')
229
354
# If filename is relative, make absolute using "root".
230
355
if not os.path.isabs(filename):
232
357
msg = "Static tool requires an absolute filename (got '%s')." % filename
359
cherrypy.log(msg, 'TOOLS.STATICFILE')
233
360
raise ValueError(msg)
234
361
filename = os.path.join(root, filename)
236
return _attempt(filename, content_types)
363
return _attempt(filename, content_types, debug=debug)