shared_data.py 9.4 KB

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