source file: /Library/Python/2.3/site-packages/CherryPy-3.0.1-py2.3.egg/cherrypy/lib/static.py
file stats: 119 lines, 17 executed: 14.3% covered
   1. import mimetools
   2. import mimetypes
   3. mimetypes.init()
   4. mimetypes.types_map['.dwg']='image/x-dwg'
   5. mimetypes.types_map['.ico']='image/x-icon'
   6. 
   7. import os
   8. import re
   9. import stat
  10. import time
  11. import urllib
  12. 
  13. import cherrypy
  14. from cherrypy.lib import cptools, http
  15. 
  16. 
  17. def serve_file(path, content_type=None, disposition=None, name=None):
  18.     """Set status, headers, and body in order to serve the given file.
  19. 
  20.     The Content-Type header will be set to the content_type arg, if provided.
  21.     If not provided, the Content-Type will be guessed by its extension.
  22. 
  23.     If disposition is not None, the Content-Disposition header will be set
  24.     to "<disposition>; filename=<name>". If name is None, it will be set
  25.     to the basename of path. If disposition is None, no Content-Disposition
  26.     header will be written.
  27.     """
  28. 
  29.     response = cherrypy.response
  30. 
  31.     # If path is relative, users should fix it by making path absolute.
  32.     # That is, CherryPy should not guess where the application root is.
  33.     # 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".
  36.     if not os.path.isabs(path):
  37.         raise ValueError("'%s' is not an absolute path." % path)
  38. 
  39.     try:
  40.         st = os.stat(path)
  41.     except OSError:
  42.         raise cherrypy.NotFound()
  43. 
  44.     # Check if path is a directory.
  45.     if stat.S_ISDIR(st.st_mode):
  46.         # Let the caller deal with it as they like.
  47.         raise cherrypy.NotFound()
  48. 
  49.     # Set the Last-Modified response header, so that
  50.     # modified-since validation code can work.
  51.     response.headers['Last-Modified'] = http.HTTPDate(st.st_mtime)
  52.     cptools.validate_since()
  53. 
  54.     if content_type is None:
  55.         # Set content-type based on filename extension
  56.         ext = ""
  57.         i = path.rfind('.')
  58.         if i != -1:
  59.             ext = path[i:].lower()
  60.         content_type = mimetypes.types_map.get(ext, "text/plain")
  61.     response.headers['Content-Type'] = content_type
  62. 
  63.     if disposition is not None:
  64.         if name is None:
  65.             name = os.path.basename(path)
  66.         cd = '%s; filename="%s"' % (disposition, name)
  67.         response.headers["Content-Disposition"] = cd
  68. 
  69.     # Set Content-Length and use an iterable (file object)
  70.     #   this way CP won't load the whole file in memory
  71.     c_len = st.st_size
  72.     bodyfile = open(path, 'rb')
  73. 
  74.     # HTTP/1.0 didn't have Range/Accept-Ranges headers, or the 206 code
  75.     if cherrypy.request.protocol >= (1, 1):
  76.         response.headers["Accept-Ranges"] = "bytes"
  77.         r = http.get_ranges(cherrypy.request.headers.get('Range'), c_len)
  78.         if r == []:
  79.             response.headers['Content-Range'] = "bytes */%s" % c_len
  80.             message = "Invalid Range (first-byte-pos greater than Content-Length)"
  81.             raise cherrypy.HTTPError(416, message)
  82.         if r:
  83.             if len(r) == 1:
  84.                 # Return a single-part response.
  85.                 start, stop = r[0]
  86.                 r_len = stop - start
  87.                 response.status = "206 Partial Content"
  88.                 response.headers['Content-Range'] = ("bytes %s-%s/%s" %
  89.                                                        (start, stop - 1, c_len))
  90.                 response.headers['Content-Length'] = r_len
  91.                 bodyfile.seek(start)
  92.                 response.body = bodyfile.read(r_len)
  93.             else:
  94.                 # Return a multipart/byteranges response.
  95.                 response.status = "206 Partial Content"
  96.                 boundary = mimetools.choose_boundary()
  97.                 ct = "multipart/byteranges; boundary=%s" % boundary
  98.                 response.headers['Content-Type'] = ct
  99.                 if response.headers.has_key("Content-Length"):
 100.                     # Delete Content-Length header so finalize() recalcs it.
 101.                     del response.headers["Content-Length"]
 102. 
 103.                 def file_ranges():
 104.                     # Apache compatibility:
 105.                     yield "\r\n"
 106. 
 107.                     for start, stop in r:
 108.                         yield "--" + boundary
 109.                         yield "\r\nContent-type: %s" % content_type
 110.                         yield ("\r\nContent-range: bytes %s-%s/%s\r\n\r\n"
 111.                                % (start, stop - 1, c_len))
 112.                         bodyfile.seek(start)
 113.                         yield bodyfile.read(stop - start)
 114.                         yield "\r\n"
 115.                     # Final boundary
 116.                     yield "--" + boundary + "--"
 117. 
 118.                     # Apache compatibility:
 119.                     yield "\r\n"
 120.                 response.body = file_ranges()
 121.         else:
 122.             response.headers['Content-Length'] = c_len
 123.             response.body = bodyfile
 124.     else:
 125.         response.headers['Content-Length'] = c_len
 126.         response.body = bodyfile
 127.     return response.body
 128. 
 129. def serve_download(path, name=None):
 130.     """Serve 'path' as an application/x-download attachment."""
 131.     # This is such a common idiom I felt it deserved its own wrapper.
 132.     return serve_file(path, "application/x-download", "attachment", name)
 133. 
 134. 
 135. def _attempt(filename, content_types):
 136.     try:
 137.         # you can set the content types for a
 138.         # complete directory per extension
 139.         content_type = None
 140.         if content_types:
 141.             r, ext = os.path.splitext(filename)
 142.             content_type = content_types.get(ext[1:], None)
 143.         serve_file(filename, content_type=content_type)
 144.         return True
 145.     except cherrypy.NotFound:
 146.         # If we didn't find the static file, continue handling the
 147.         # request. We might find a dynamic handler instead.
 148.         return False
 149. 
 150. def staticdir(section, dir, root="", match="", content_types=None, index=""):
 151.     """Serve a static resource from the given (root +) dir."""
 152.     if match and not re.search(match, cherrypy.request.path_info):
 153.         return False
 154. 
 155.     # If dir is relative, make absolute using "root".
 156.     if not os.path.isabs(dir):
 157.         if not root:
 158.             msg = "Static dir requires an absolute dir (or root)."
 159.             raise ValueError(msg)
 160.         dir = os.path.join(root, dir)
 161. 
 162.     # Determine where we are in the object tree relative to 'section'
 163.     # (where the static tool was defined).
 164.     if section == 'global':
 165.         section = "/"
 166.     section = section.rstrip(r"\/")
 167.     branch = cherrypy.request.path_info[len(section) + 1:]
 168.     branch = urllib.unquote(branch.lstrip(r"\/"))
 169. 
 170.     # If branch is "", filename will end in a slash
 171.     filename = os.path.join(dir, branch)
 172. 
 173.     # There's a chance that the branch pulled from the URL might
 174.     # have ".." or similar uplevel attacks in it. Check that the final
 175.     # filename is a child of dir.
 176.     if not os.path.normpath(filename).startswith(os.path.normpath(dir)):
 177.         raise cherrypy.HTTPError(403) # Forbidden
 178. 
 179.     handled = _attempt(filename, content_types)
 180.     if not handled:
 181.         # Check for an index file if a folder was requested.
 182.         if index and filename[-1] in (r"\/"):
 183.             handled = _attempt(os.path.join(filename, index), content_types)
 184.     return handled
 185. 
 186. def staticfile(filename, root=None, match="", content_types=None):
 187.     """Serve a static resource from the given (root +) filename."""
 188.     if match and not re.search(match, cherrypy.request.path_info):
 189.         return False
 190. 
 191.     # If filename is relative, make absolute using "root".
 192.     if not os.path.isabs(filename):
 193.         if not root:
 194.             msg = "Static tool requires an absolute filename (got '%s')." % filename
 195.             raise ValueError(msg)
 196.         filename = os.path.join(root, filename)
 197. 
 198.     return _attempt(filename, content_types)