1 try:
2 from io import UnsupportedOperation
3 except ImportError:
4 UnsupportedOperation = object()
5 import logging
6 import mimetypes
7 mimetypes.init()
8 mimetypes.types_map['.dwg'] = 'image/x-dwg'
9 mimetypes.types_map['.ico'] = 'image/x-icon'
10 mimetypes.types_map['.bz2'] = 'application/x-bzip2'
11 mimetypes.types_map['.gz'] = 'application/x-gzip'
12
13 import os
14 import re
15 import stat
16 import time
17
18 import cherrypy
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,
24 debug=False):
25 """Set status, headers, and body in order to serve the given path.
26
27 The Content-Type header will be set to the content_type arg, if provided.
28 If not provided, the Content-Type will be guessed by the file extension
29 of the 'path' argument.
30
31 If disposition is not None, the Content-Disposition header will be set
32 to "<disposition>; filename=<name>". If name is None, it will be set
33 to the basename of path. If disposition is None, no Content-Disposition
34 header will be written.
35 """
36
37 response = cherrypy.serving.response
38
39
40
41
42
43
44 if not os.path.isabs(path):
45 msg = "'%s' is not an absolute path." % path
46 if debug:
47 cherrypy.log(msg, 'TOOLS.STATICFILE')
48 raise ValueError(msg)
49
50 try:
51 st = os.stat(path)
52 except OSError:
53 if debug:
54 cherrypy.log('os.stat(%r) failed' % path, 'TOOLS.STATIC')
55 raise cherrypy.NotFound()
56
57
58 if stat.S_ISDIR(st.st_mode):
59
60 if debug:
61 cherrypy.log('%r is a directory' % path, 'TOOLS.STATIC')
62 raise cherrypy.NotFound()
63
64
65
66 response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime)
67 cptools.validate_since()
68
69 if content_type is None:
70
71 ext = ""
72 i = path.rfind('.')
73 if i != -1:
74 ext = path[i:].lower()
75 content_type = mimetypes.types_map.get(ext, None)
76 if content_type is not None:
77 response.headers['Content-Type'] = content_type
78 if debug:
79 cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC')
80
81 cd = None
82 if disposition is not None:
83 if name is None:
84 name = os.path.basename(path)
85 cd = '%s; filename="%s"' % (disposition, name)
86 response.headers["Content-Disposition"] = cd
87 if debug:
88 cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
89
90
91
92 content_length = st.st_size
93 fileobj = open(path, 'rb')
94 return _serve_fileobj(fileobj, content_type, content_length, debug=debug)
95
96
97 -def serve_fileobj(fileobj, content_type=None, disposition=None, name=None,
98 debug=False):
99 """Set status, headers, and body in order to serve the given file object.
100
101 The Content-Type header will be set to the content_type arg, if provided.
102
103 If disposition is not None, the Content-Disposition header will be set
104 to "<disposition>; filename=<name>". If name is None, 'filename' will
105 not be set. If disposition is None, no Content-Disposition header will
106 be written.
107
108 CAUTION: If the request contains a 'Range' header, one or more seek()s will
109 be performed on the file object. This may cause undesired behavior if
110 the file object is not seekable. It could also produce undesired results
111 if the caller set the read position of the file object prior to calling
112 serve_fileobj(), expecting that the data would be served starting from that
113 position.
114 """
115
116 response = cherrypy.serving.response
117
118 try:
119 st = os.fstat(fileobj.fileno())
120 except AttributeError:
121 if debug:
122 cherrypy.log('os has no fstat attribute', 'TOOLS.STATIC')
123 content_length = None
124 except UnsupportedOperation:
125 content_length = None
126 else:
127
128
129 response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime)
130 cptools.validate_since()
131 content_length = st.st_size
132
133 if content_type is not None:
134 response.headers['Content-Type'] = content_type
135 if debug:
136 cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC')
137
138 cd = None
139 if disposition is not None:
140 if name is None:
141 cd = disposition
142 else:
143 cd = '%s; filename="%s"' % (disposition, name)
144 response.headers["Content-Disposition"] = cd
145 if debug:
146 cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
147
148 return _serve_fileobj(fileobj, content_type, content_length, debug=debug)
149
150
151 -def _serve_fileobj(fileobj, content_type, content_length, debug=False):
152 """Internal. Set response.body to the given file object, perhaps ranged."""
153 response = cherrypy.serving.response
154
155
156 request = cherrypy.serving.request
157 if request.protocol >= (1, 1):
158 response.headers["Accept-Ranges"] = "bytes"
159 r = httputil.get_ranges(request.headers.get('Range'), content_length)
160 if r == []:
161 response.headers['Content-Range'] = "bytes */%s" % content_length
162 message = ("Invalid Range (first-byte-pos greater than "
163 "Content-Length)")
164 if debug:
165 cherrypy.log(message, 'TOOLS.STATIC')
166 raise cherrypy.HTTPError(416, message)
167
168 if r:
169 if len(r) == 1:
170
171 start, stop = r[0]
172 if stop > content_length:
173 stop = content_length
174 r_len = stop - start
175 if debug:
176 cherrypy.log(
177 'Single part; start: %r, stop: %r' % (start, stop),
178 'TOOLS.STATIC')
179 response.status = "206 Partial Content"
180 response.headers['Content-Range'] = (
181 "bytes %s-%s/%s" % (start, stop - 1, content_length))
182 response.headers['Content-Length'] = r_len
183 fileobj.seek(start)
184 response.body = file_generator_limited(fileobj, r_len)
185 else:
186
187 response.status = "206 Partial Content"
188 try:
189
190 from email.generator import _make_boundary as make_boundary
191 except ImportError:
192
193 from mimetools import choose_boundary as make_boundary
194 boundary = make_boundary()
195 ct = "multipart/byteranges; boundary=%s" % boundary
196 response.headers['Content-Type'] = ct
197 if "Content-Length" in response.headers:
198
199 del response.headers["Content-Length"]
200
201 def file_ranges():
202
203 yield ntob("\r\n")
204
205 for start, stop in r:
206 if debug:
207 cherrypy.log(
208 'Multipart; start: %r, stop: %r' % (
209 start, stop),
210 'TOOLS.STATIC')
211 yield ntob("--" + boundary, 'ascii')
212 yield ntob("\r\nContent-type: %s" % content_type,
213 'ascii')
214 yield ntob(
215 "\r\nContent-range: bytes %s-%s/%s\r\n\r\n" % (
216 start, stop - 1, content_length),
217 'ascii')
218 fileobj.seek(start)
219 gen = file_generator_limited(fileobj, stop - start)
220 for chunk in gen:
221 yield chunk
222 yield ntob("\r\n")
223
224 yield ntob("--" + boundary + "--", 'ascii')
225
226
227 yield ntob("\r\n")
228 response.body = file_ranges()
229 return response.body
230 else:
231 if debug:
232 cherrypy.log('No byteranges requested', 'TOOLS.STATIC')
233
234
235
236 response.headers['Content-Length'] = content_length
237 response.body = fileobj
238 return response.body
239
240
242 """Serve 'path' as an application/x-download attachment."""
243
244 return serve_file(path, "application/x-download", "attachment", name)
245
246
247 -def _attempt(filename, content_types, debug=False):
266
267
268 -def staticdir(section, dir, root="", match="", content_types=None, index="",
269 debug=False):
270 """Serve a static resource from the given (root +) dir.
271
272 match
273 If given, request.path_info will be searched for the given
274 regular expression before attempting to serve static content.
275
276 content_types
277 If given, it should be a Python dictionary of
278 {file-extension: content-type} pairs, where 'file-extension' is
279 a string (e.g. "gif") and 'content-type' is the value to write
280 out in the Content-Type response header (e.g. "image/gif").
281
282 index
283 If provided, it should be the (relative) name of a file to
284 serve for directory requests. For example, if the dir argument is
285 '/home/me', the Request-URI is 'myapp', and the index arg is
286 'index.html', the file '/home/me/myapp/index.html' will be sought.
287 """
288 request = cherrypy.serving.request
289 if request.method not in ('GET', 'HEAD'):
290 if debug:
291 cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICDIR')
292 return False
293
294 if match and not re.search(match, request.path_info):
295 if debug:
296 cherrypy.log('request.path_info %r does not match pattern %r' %
297 (request.path_info, match), 'TOOLS.STATICDIR')
298 return False
299
300
301 dir = os.path.expanduser(dir)
302
303
304 if not os.path.isabs(dir):
305 if not root:
306 msg = "Static dir requires an absolute dir (or root)."
307 if debug:
308 cherrypy.log(msg, 'TOOLS.STATICDIR')
309 raise ValueError(msg)
310 dir = os.path.join(root, dir)
311
312
313
314 if section == 'global':
315 section = "/"
316 section = section.rstrip(r"\/")
317 branch = request.path_info[len(section) + 1:]
318 branch = unquote(branch.lstrip(r"\/"))
319
320
321 filename = os.path.join(dir, branch)
322 if debug:
323 cherrypy.log('Checking file %r to fulfill %r' %
324 (filename, request.path_info), 'TOOLS.STATICDIR')
325
326
327
328
329 if not os.path.normpath(filename).startswith(os.path.normpath(dir)):
330 raise cherrypy.HTTPError(403)
331
332 handled = _attempt(filename, content_types)
333 if not handled:
334
335 if index:
336 handled = _attempt(os.path.join(filename, index), content_types)
337 if handled:
338 request.is_index = filename[-1] in (r"\/")
339 return handled
340
341
342 -def staticfile(filename, root=None, match="", content_types=None, debug=False):
343 """Serve a static resource from the given (root +) filename.
344
345 match
346 If given, request.path_info will be searched for the given
347 regular expression before attempting to serve static content.
348
349 content_types
350 If given, it should be a Python dictionary of
351 {file-extension: content-type} pairs, where 'file-extension' is
352 a string (e.g. "gif") and 'content-type' is the value to write
353 out in the Content-Type response header (e.g. "image/gif").
354
355 """
356 request = cherrypy.serving.request
357 if request.method not in ('GET', 'HEAD'):
358 if debug:
359 cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICFILE')
360 return False
361
362 if match and not re.search(match, request.path_info):
363 if debug:
364 cherrypy.log('request.path_info %r does not match pattern %r' %
365 (request.path_info, match), 'TOOLS.STATICFILE')
366 return False
367
368
369 if not os.path.isabs(filename):
370 if not root:
371 msg = "Static tool requires an absolute filename (got '%s')." % (
372 filename,)
373 if debug:
374 cherrypy.log(msg, 'TOOLS.STATICFILE')
375 raise ValueError(msg)
376 filename = os.path.join(root, filename)
377
378 return _attempt(filename, content_types, debug=debug)
379