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, debug=False):
24 """Set status, headers, and body in order to serve the given path.
25
26 The Content-Type header will be set to the content_type arg, if provided.
27 If not provided, the Content-Type will be guessed by the file extension
28 of the 'path' argument.
29
30 If disposition is not None, the Content-Disposition header will be set
31 to "<disposition>; filename=<name>". If name is None, it will be set
32 to the basename of path. If disposition is None, no Content-Disposition
33 header will be written.
34 """
35
36 response = cherrypy.serving.response
37
38
39
40
41
42
43 if not os.path.isabs(path):
44 msg = "'%s' is not an absolute path." % path
45 if debug:
46 cherrypy.log(msg, 'TOOLS.STATICFILE')
47 raise ValueError(msg)
48
49 try:
50 st = os.stat(path)
51 except OSError:
52 if debug:
53 cherrypy.log('os.stat(%r) failed' % path, 'TOOLS.STATIC')
54 raise cherrypy.NotFound()
55
56
57 if stat.S_ISDIR(st.st_mode):
58
59 if debug:
60 cherrypy.log('%r is a directory' % path, 'TOOLS.STATIC')
61 raise cherrypy.NotFound()
62
63
64
65 response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime)
66 cptools.validate_since()
67
68 if content_type is None:
69
70 ext = ""
71 i = path.rfind('.')
72 if i != -1:
73 ext = path[i:].lower()
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')
79
80 cd = None
81 if disposition is not None:
82 if name is None:
83 name = os.path.basename(path)
84 cd = '%s; filename="%s"' % (disposition, name)
85 response.headers["Content-Disposition"] = cd
86 if debug:
87 cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
88
89
90
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
126
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
151
152
153 request = cherrypy.serving.request
154 if request.protocol >= (1, 1):
155 response.headers["Accept-Ranges"] = "bytes"
156 r = httputil.get_ranges(request.headers.get('Range'), content_length)
157 if r == []:
158 response.headers['Content-Range'] = "bytes */%s" % content_length
159 message = "Invalid Range (first-byte-pos greater than Content-Length)"
160 if debug:
161 cherrypy.log(message, 'TOOLS.STATIC')
162 raise cherrypy.HTTPError(416, message)
163
164 if r:
165 if len(r) == 1:
166
167 start, stop = r[0]
168 if stop > content_length:
169 stop = content_length
170 r_len = stop - start
171 if debug:
172 cherrypy.log('Single part; start: %r, stop: %r' % (start, stop),
173 'TOOLS.STATIC')
174 response.status = "206 Partial Content"
175 response.headers['Content-Range'] = (
176 "bytes %s-%s/%s" % (start, stop - 1, content_length))
177 response.headers['Content-Length'] = r_len
178 fileobj.seek(start)
179 response.body = file_generator_limited(fileobj, r_len)
180 else:
181
182 response.status = "206 Partial Content"
183 try:
184
185 from email.generator import _make_boundary as choose_boundary
186 except ImportError:
187
188 from mimetools import choose_boundary
189 boundary = choose_boundary()
190 ct = "multipart/byteranges; boundary=%s" % boundary
191 response.headers['Content-Type'] = ct
192 if "Content-Length" in response.headers:
193
194 del response.headers["Content-Length"]
195
196 def file_ranges():
197
198 yield ntob("\r\n")
199
200 for start, stop in r:
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):
210 yield chunk
211 yield ntob("\r\n")
212
213 yield ntob("--" + boundary + "--", 'ascii')
214
215
216 yield ntob("\r\n")
217 response.body = file_ranges()
218 return response.body
219 else:
220 if debug:
221 cherrypy.log('No byteranges requested', 'TOOLS.STATIC')
222
223
224
225 response.headers['Content-Length'] = content_length
226 response.body = fileobj
227 return response.body
228
230 """Serve 'path' as an application/x-download attachment."""
231
232 return serve_file(path, "application/x-download", "attachment", name)
233
234
235 -def _attempt(filename, content_types, debug=False):
254
255 -def staticdir(section, dir, root="", match="", content_types=None, index="",
256 debug=False):
257 """Serve a static resource from the given (root +) dir.
258
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.
274 """
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')
279 return False
280
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')
285 return False
286
287
288 dir = os.path.expanduser(dir)
289
290
291 if not os.path.isabs(dir):
292 if not root:
293 msg = "Static dir requires an absolute dir (or root)."
294 if debug:
295 cherrypy.log(msg, 'TOOLS.STATICDIR')
296 raise ValueError(msg)
297 dir = os.path.join(root, dir)
298
299
300
301 if section == 'global':
302 section = "/"
303 section = section.rstrip(r"\/")
304 branch = request.path_info[len(section) + 1:]
305 branch = unquote(branch.lstrip(r"\/"))
306
307
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')
312
313
314
315
316 if not os.path.normpath(filename).startswith(os.path.normpath(dir)):
317 raise cherrypy.HTTPError(403)
318
319 handled = _attempt(filename, content_types)
320 if not handled:
321
322 if index:
323 handled = _attempt(os.path.join(filename, index), content_types)
324 if handled:
325 request.is_index = filename[-1] in (r"\/")
326 return handled
327
328 -def staticfile(filename, root=None, match="", content_types=None, debug=False):
329 """Serve a static resource from the given (root +) filename.
330
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
341 """
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')
346 return False
347
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')
352 return False
353
354
355 if not os.path.isabs(filename):
356 if not root:
357 msg = "Static tool requires an absolute filename (got '%s')." % filename
358 if debug:
359 cherrypy.log(msg, 'TOOLS.STATICFILE')
360 raise ValueError(msg)
361 filename = os.path.join(root, filename)
362
363 return _attempt(filename, content_types, debug=debug)
364