shared_data.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. """
  2. Serve Shared Static Files
  3. =========================
  4. .. autoclass:: SharedDataMiddleware
  5. :members: is_allowed
  6. :copyright: 2007 Pallets
  7. :license: BSD-3-Clause
  8. """
  9. import mimetypes
  10. import os
  11. import posixpath
  12. from datetime import datetime
  13. from io import BytesIO
  14. from time import mktime
  15. from time import time
  16. from zlib import adler32
  17. from .._compat import PY2
  18. from .._compat import string_types
  19. from ..filesystem import get_filesystem_encoding
  20. from ..http import http_date
  21. from ..http import is_resource_modified
  22. from ..security import safe_join
  23. from ..wsgi import get_path_info
  24. from ..wsgi import wrap_file
  25. class SharedDataMiddleware(object):
  26. """A WSGI middleware that provides static content for development
  27. environments or simple server setups. Usage is quite simple::
  28. import os
  29. from werkzeug.wsgi import SharedDataMiddleware
  30. app = SharedDataMiddleware(app, {
  31. '/static': os.path.join(os.path.dirname(__file__), 'static')
  32. })
  33. The contents of the folder ``./shared`` will now be available on
  34. ``http://example.com/shared/``. This is pretty useful during development
  35. because a standalone media server is not required. One can also mount
  36. files on the root folder and still continue to use the application because
  37. the shared data middleware forwards all unhandled requests to the
  38. application, even if the requests are below one of the shared folders.
  39. If `pkg_resources` is available you can also tell the middleware to serve
  40. files from package data::
  41. app = SharedDataMiddleware(app, {
  42. '/static': ('myapplication', 'static')
  43. })
  44. This will then serve the ``static`` folder in the `myapplication`
  45. Python package.
  46. The optional `disallow` parameter can be a list of :func:`~fnmatch.fnmatch`
  47. rules for files that are not accessible from the web. If `cache` is set to
  48. `False` no caching headers are sent.
  49. Currently the middleware does not support non ASCII filenames. If the
  50. encoding on the file system happens to be the encoding of the URI it may
  51. work but this could also be by accident. We strongly suggest using ASCII
  52. only file names for static files.
  53. The middleware will guess the mimetype using the Python `mimetype`
  54. module. If it's unable to figure out the charset it will fall back
  55. to `fallback_mimetype`.
  56. .. versionchanged:: 0.5
  57. The cache timeout is configurable now.
  58. .. versionadded:: 0.6
  59. The `fallback_mimetype` parameter was added.
  60. :param app: the application to wrap. If you don't want to wrap an
  61. application you can pass it :exc:`NotFound`.
  62. :param exports: a list or dict of exported files and folders.
  63. :param disallow: a list of :func:`~fnmatch.fnmatch` rules.
  64. :param fallback_mimetype: the fallback mimetype for unknown files.
  65. :param cache: enable or disable caching headers.
  66. :param cache_timeout: the cache timeout in seconds for the headers.
  67. """
  68. def __init__(
  69. self,
  70. app,
  71. exports,
  72. disallow=None,
  73. cache=True,
  74. cache_timeout=60 * 60 * 12,
  75. fallback_mimetype="text/plain",
  76. ):
  77. self.app = app
  78. self.exports = []
  79. self.cache = cache
  80. self.cache_timeout = cache_timeout
  81. if hasattr(exports, "items"):
  82. exports = exports.items()
  83. for key, value in exports:
  84. if isinstance(value, tuple):
  85. loader = self.get_package_loader(*value)
  86. elif isinstance(value, string_types):
  87. if os.path.isfile(value):
  88. loader = self.get_file_loader(value)
  89. else:
  90. loader = self.get_directory_loader(value)
  91. else:
  92. raise TypeError("unknown def %r" % value)
  93. self.exports.append((key, loader))
  94. if disallow is not None:
  95. from fnmatch import fnmatch
  96. self.is_allowed = lambda x: not fnmatch(x, disallow)
  97. self.fallback_mimetype = fallback_mimetype
  98. def is_allowed(self, filename):
  99. """Subclasses can override this method to disallow the access to
  100. certain files. However by providing `disallow` in the constructor
  101. this method is overwritten.
  102. """
  103. return True
  104. def _opener(self, filename):
  105. return lambda: (
  106. open(filename, "rb"),
  107. datetime.utcfromtimestamp(os.path.getmtime(filename)),
  108. int(os.path.getsize(filename)),
  109. )
  110. def get_file_loader(self, filename):
  111. return lambda x: (os.path.basename(filename), self._opener(filename))
  112. def get_package_loader(self, package, package_path):
  113. from pkg_resources import DefaultProvider, ResourceManager, get_provider
  114. loadtime = datetime.utcnow()
  115. provider = get_provider(package)
  116. manager = ResourceManager()
  117. filesystem_bound = isinstance(provider, DefaultProvider)
  118. def loader(path):
  119. if path is None:
  120. return None, None
  121. path = safe_join(package_path, path)
  122. if not provider.has_resource(path):
  123. return None, None
  124. basename = posixpath.basename(path)
  125. if filesystem_bound:
  126. return (
  127. basename,
  128. self._opener(provider.get_resource_filename(manager, path)),
  129. )
  130. s = provider.get_resource_string(manager, path)
  131. return basename, lambda: (BytesIO(s), loadtime, len(s))
  132. return loader
  133. def get_directory_loader(self, directory):
  134. def loader(path):
  135. if path is not None:
  136. path = safe_join(directory, path)
  137. else:
  138. path = directory
  139. if os.path.isfile(path):
  140. return os.path.basename(path), self._opener(path)
  141. return None, None
  142. return loader
  143. def generate_etag(self, mtime, file_size, real_filename):
  144. if not isinstance(real_filename, bytes):
  145. real_filename = real_filename.encode(get_filesystem_encoding())
  146. return "wzsdm-%d-%s-%s" % (
  147. mktime(mtime.timetuple()),
  148. file_size,
  149. adler32(real_filename) & 0xFFFFFFFF,
  150. )
  151. def __call__(self, environ, start_response):
  152. path = get_path_info(environ)
  153. if PY2:
  154. path = path.encode(get_filesystem_encoding())
  155. file_loader = None
  156. for search_path, loader in self.exports:
  157. if search_path == path:
  158. real_filename, file_loader = loader(None)
  159. if file_loader is not None:
  160. break
  161. if not search_path.endswith("/"):
  162. search_path += "/"
  163. if path.startswith(search_path):
  164. real_filename, file_loader = loader(path[len(search_path) :])
  165. if file_loader is not None:
  166. break
  167. if file_loader is None or not self.is_allowed(real_filename):
  168. return self.app(environ, start_response)
  169. guessed_type = mimetypes.guess_type(real_filename)
  170. mime_type = guessed_type[0] or self.fallback_mimetype
  171. f, mtime, file_size = file_loader()
  172. headers = [("Date", http_date())]
  173. if self.cache:
  174. timeout = self.cache_timeout
  175. etag = self.generate_etag(mtime, file_size, real_filename)
  176. headers += [
  177. ("Etag", '"%s"' % etag),
  178. ("Cache-Control", "max-age=%d, public" % timeout),
  179. ]
  180. if not is_resource_modified(environ, etag, last_modified=mtime):
  181. f.close()
  182. start_response("304 Not Modified", headers)
  183. return []
  184. headers.append(("Expires", http_date(time() + timeout)))
  185. else:
  186. headers.append(("Cache-Control", "public"))
  187. headers.extend(
  188. (
  189. ("Content-Type", mime_type),
  190. ("Content-Length", str(file_size)),
  191. ("Last-Modified", http_date(mtime)),
  192. )
  193. )
  194. start_response("200 OK", headers)
  195. return wrap_file(environ, f)