4 mimetypes
.types_map
['.dwg']='image/x-dwg'
5 mimetypes
.types_map
['.ico']='image/x-icon'
6 mimetypes
.types_map
['.bz2']='application/x-bzip2'
7 mimetypes
.types_map
['.gz']='application/x-gzip'
15 from cherrypy
._cpcompat
import ntob
, unquote
16 from cherrypy
.lib
import cptools
, httputil
, file_generator_limited
19 def serve_file(path
, content_type
=None, disposition
=None, name
=None, debug
=False):
20 """Set status, headers, and body in order to serve the given path.
22 The Content-Type header will be set to the content_type arg, if provided.
23 If not provided, the Content-Type will be guessed by the file extension
24 of the 'path' argument.
26 If disposition is not None, the Content-Disposition header will be set
27 to "<disposition>; filename=<name>". If name is None, it will be set
28 to the basename of path. If disposition is None, no Content-Disposition
29 header will be written.
32 response
= cherrypy
.serving
.response
34 # If path is relative, users should fix it by making path absolute.
35 # That is, CherryPy should not guess where the application root is.
36 # It certainly should *not* use cwd (since CP may be invoked from a
37 # variety of paths). If using tools.staticdir, you can make your relative
38 # paths become absolute by supplying a value for "tools.staticdir.root".
39 if not os
.path
.isabs(path
):
40 msg
= "'%s' is not an absolute path." % path
42 cherrypy
.log(msg
, 'TOOLS.STATICFILE')
49 cherrypy
.log('os.stat(%r) failed' % path
, 'TOOLS.STATIC')
50 raise cherrypy
.NotFound()
52 # Check if path is a directory.
53 if stat
.S_ISDIR(st
.st_mode
):
54 # Let the caller deal with it as they like.
56 cherrypy
.log('%r is a directory' % path
, 'TOOLS.STATIC')
57 raise cherrypy
.NotFound()
59 # Set the Last-Modified response header, so that
60 # modified-since validation code can work.
61 response
.headers
['Last-Modified'] = httputil
.HTTPDate(st
.st_mtime
)
62 cptools
.validate_since()
64 if content_type
is None:
65 # Set content-type based on filename extension
69 ext
= path
[i
:].lower()
70 content_type
= mimetypes
.types_map
.get(ext
, None)
71 if content_type
is not None:
72 response
.headers
['Content-Type'] = content_type
74 cherrypy
.log('Content-Type: %r' % content_type
, 'TOOLS.STATIC')
77 if disposition
is not None:
79 name
= os
.path
.basename(path
)
80 cd
= '%s; filename="%s"' % (disposition
, name
)
81 response
.headers
["Content-Disposition"] = cd
83 cherrypy
.log('Content-Disposition: %r' % cd
, 'TOOLS.STATIC')
85 # Set Content-Length and use an iterable (file object)
86 # this way CP won't load the whole file in memory
87 content_length
= st
.st_size
88 fileobj
= open(path
, 'rb')
89 return _serve_fileobj(fileobj
, content_type
, content_length
, debug
=debug
)
91 def serve_fileobj(fileobj
, content_type
=None, disposition
=None, name
=None,
93 """Set status, headers, and body in order to serve the given file object.
95 The Content-Type header will be set to the content_type arg, if provided.
97 If disposition is not None, the Content-Disposition header will be set
98 to "<disposition>; filename=<name>". If name is None, 'filename' will
99 not be set. If disposition is None, no Content-Disposition header will
102 CAUTION: If the request contains a 'Range' header, one or more seek()s will
103 be performed on the file object. This may cause undesired behavior if
104 the file object is not seekable. It could also produce undesired results
105 if the caller set the read position of the file object prior to calling
106 serve_fileobj(), expecting that the data would be served starting from that
110 response
= cherrypy
.serving
.response
113 st
= os
.fstat(fileobj
.fileno())
114 except AttributeError:
116 cherrypy
.log('os has no fstat attribute', 'TOOLS.STATIC')
117 content_length
= None
119 # Set the Last-Modified response header, so that
120 # modified-since validation code can work.
121 response
.headers
['Last-Modified'] = httputil
.HTTPDate(st
.st_mtime
)
122 cptools
.validate_since()
123 content_length
= st
.st_size
125 if content_type
is not None:
126 response
.headers
['Content-Type'] = content_type
128 cherrypy
.log('Content-Type: %r' % content_type
, 'TOOLS.STATIC')
131 if disposition
is not None:
135 cd
= '%s; filename="%s"' % (disposition
, name
)
136 response
.headers
["Content-Disposition"] = cd
138 cherrypy
.log('Content-Disposition: %r' % cd
, 'TOOLS.STATIC')
140 return _serve_fileobj(fileobj
, content_type
, content_length
, debug
=debug
)
142 def _serve_fileobj(fileobj
, content_type
, content_length
, debug
=False):
143 """Internal. Set response.body to the given file object, perhaps ranged."""
144 response
= cherrypy
.serving
.response
146 # HTTP/1.0 didn't have Range/Accept-Ranges headers, or the 206 code
147 request
= cherrypy
.serving
.request
148 if request
.protocol
>= (1, 1):
149 response
.headers
["Accept-Ranges"] = "bytes"
150 r
= httputil
.get_ranges(request
.headers
.get('Range'), content_length
)
152 response
.headers
['Content-Range'] = "bytes */%s" % content_length
153 message
= "Invalid Range (first-byte-pos greater than Content-Length)"
155 cherrypy
.log(message
, 'TOOLS.STATIC')
156 raise cherrypy
.HTTPError(416, message
)
160 # Return a single-part response.
162 if stop
> content_length
:
163 stop
= content_length
166 cherrypy
.log('Single part; start: %r, stop: %r' % (start
, stop
),
168 response
.status
= "206 Partial Content"
169 response
.headers
['Content-Range'] = (
170 "bytes %s-%s/%s" % (start
, stop
- 1, content_length
))
171 response
.headers
['Content-Length'] = r_len
173 response
.body
= file_generator_limited(fileobj
, r_len
)
175 # Return a multipart/byteranges response.
176 response
.status
= "206 Partial Content"
177 from mimetools
import choose_boundary
178 boundary
= choose_boundary()
179 ct
= "multipart/byteranges; boundary=%s" % boundary
180 response
.headers
['Content-Type'] = ct
181 if "Content-Length" in response
.headers
:
182 # Delete Content-Length header so finalize() recalcs it.
183 del response
.headers
["Content-Length"]
186 # Apache compatibility:
189 for start
, stop
in r
:
191 cherrypy
.log('Multipart; start: %r, stop: %r' % (start
, stop
),
193 yield ntob("--" + boundary
, 'ascii')
194 yield ntob("\r\nContent-type: %s" % content_type
, 'ascii')
195 yield ntob("\r\nContent-range: bytes %s-%s/%s\r\n\r\n"
196 % (start
, stop
- 1, content_length
), 'ascii')
198 for chunk
in file_generator_limited(fileobj
, stop
-start
):
202 yield ntob("--" + boundary
+ "--", 'ascii')
204 # Apache compatibility:
206 response
.body
= file_ranges()
210 cherrypy
.log('No byteranges requested', 'TOOLS.STATIC')
212 # Set Content-Length and use an iterable (file object)
213 # this way CP won't load the whole file in memory
214 response
.headers
['Content-Length'] = content_length
215 response
.body
= fileobj
218 def serve_download(path
, name
=None):
219 """Serve 'path' as an application/x-download attachment."""
220 # This is such a common idiom I felt it deserved its own wrapper.
221 return serve_file(path
, "application/x-download", "attachment", name
)
224 def _attempt(filename
, content_types
, debug
=False):
226 cherrypy
.log('Attempting %r (content_types %r)' %
227 (filename
, content_types
), 'TOOLS.STATICDIR')
229 # you can set the content types for a
230 # complete directory per extension
233 r
, ext
= os
.path
.splitext(filename
)
234 content_type
= content_types
.get(ext
[1:], None)
235 serve_file(filename
, content_type
=content_type
, debug
=debug
)
237 except cherrypy
.NotFound
:
238 # If we didn't find the static file, continue handling the
239 # request. We might find a dynamic handler instead.
241 cherrypy
.log('NotFound', 'TOOLS.STATICFILE')
244 def staticdir(section
, dir, root
="", match
="", content_types
=None, index
="",
246 """Serve a static resource from the given (root +) dir.
249 If given, request.path_info will be searched for the given
250 regular expression before attempting to serve static content.
253 If given, it should be a Python dictionary of
254 {file-extension: content-type} pairs, where 'file-extension' is
255 a string (e.g. "gif") and 'content-type' is the value to write
256 out in the Content-Type response header (e.g. "image/gif").
259 If provided, it should be the (relative) name of a file to
260 serve for directory requests. For example, if the dir argument is
261 '/home/me', the Request-URI is 'myapp', and the index arg is
262 'index.html', the file '/home/me/myapp/index.html' will be sought.
264 request
= cherrypy
.serving
.request
265 if request
.method
not in ('GET', 'HEAD'):
267 cherrypy
.log('request.method not GET or HEAD', 'TOOLS.STATICDIR')
270 if match
and not re
.search(match
, request
.path_info
):
272 cherrypy
.log('request.path_info %r does not match pattern %r' %
273 (request
.path_info
, match
), 'TOOLS.STATICDIR')
276 # Allow the use of '~' to refer to a user's home directory.
277 dir = os
.path
.expanduser(dir)
279 # If dir is relative, make absolute using "root".
280 if not os
.path
.isabs(dir):
282 msg
= "Static dir requires an absolute dir (or root)."
284 cherrypy
.log(msg
, 'TOOLS.STATICDIR')
285 raise ValueError(msg
)
286 dir = os
.path
.join(root
, dir)
288 # Determine where we are in the object tree relative to 'section'
289 # (where the static tool was defined).
290 if section
== 'global':
292 section
= section
.rstrip(r
"\/")
293 branch
= request
.path_info
[len(section
) + 1:]
294 branch
= unquote(branch
.lstrip(r
"\/"))
296 # If branch is "", filename will end in a slash
297 filename
= os
.path
.join(dir, branch
)
299 cherrypy
.log('Checking file %r to fulfill %r' %
300 (filename
, request
.path_info
), 'TOOLS.STATICDIR')
302 # There's a chance that the branch pulled from the URL might
303 # have ".." or similar uplevel attacks in it. Check that the final
304 # filename is a child of dir.
305 if not os
.path
.normpath(filename
).startswith(os
.path
.normpath(dir)):
306 raise cherrypy
.HTTPError(403) # Forbidden
308 handled
= _attempt(filename
, content_types
)
310 # Check for an index file if a folder was requested.
312 handled
= _attempt(os
.path
.join(filename
, index
), content_types
)
314 request
.is_index
= filename
[-1] in (r
"\/")
317 def staticfile(filename
, root
=None, match
="", content_types
=None, debug
=False):
318 """Serve a static resource from the given (root +) filename.
321 If given, request.path_info will be searched for the given
322 regular expression before attempting to serve static content.
325 If given, it should be a Python dictionary of
326 {file-extension: content-type} pairs, where 'file-extension' is
327 a string (e.g. "gif") and 'content-type' is the value to write
328 out in the Content-Type response header (e.g. "image/gif").
331 request
= cherrypy
.serving
.request
332 if request
.method
not in ('GET', 'HEAD'):
334 cherrypy
.log('request.method not GET or HEAD', 'TOOLS.STATICFILE')
337 if match
and not re
.search(match
, request
.path_info
):
339 cherrypy
.log('request.path_info %r does not match pattern %r' %
340 (request
.path_info
, match
), 'TOOLS.STATICFILE')
343 # If filename is relative, make absolute using "root".
344 if not os
.path
.isabs(filename
):
346 msg
= "Static tool requires an absolute filename (got '%s')." % filename
348 cherrypy
.log(msg
, 'TOOLS.STATICFILE')
349 raise ValueError(msg
)
350 filename
= os
.path
.join(root
, filename
)
352 return _attempt(filename
, content_types
, debug
=debug
)