pax_global_header00006660000000000000000000000064131615124630014514gustar00rootroot0000000000000052 comment=9238341a348c57e20a0f4bab34fb93b86c7d59be whitenoise-3.3.1/000077500000000000000000000000001316151246300136765ustar00rootroot00000000000000whitenoise-3.3.1/.gitignore000066400000000000000000000001451316151246300156660ustar00rootroot00000000000000*.py[co] __pycache__ /MANIFEST /.tox /.coverage /.coverage.* /htmlcov /docs/_build /dist /*.egg-info whitenoise-3.3.1/.travis.yml000066400000000000000000000023071316151246300160110ustar00rootroot00000000000000# Use the faster container-based infrastructure. sudo: false language: python python: - 2.7 - 3.4 - 3.5 - pypy install: travis_retry pip install 'requests>=2.11,<2.12' $DJANGO_VERSION env: global: - PYTHONWARNINGS=all DJANGO_SETTINGS_MODULE=tests.django_settings matrix: - DJANGO_VERSION='Django>=1.8,<1.9' - DJANGO_VERSION='Django>=1.9,<1.10' - DJANGO_VERSION='Django>=1.10,<1.11' - DJANGO_VERSION='Django>=1.11a1,<2.0' matrix: include: - python: 3.3 env: DJANGO_VERSION='Django>=1.8,<1.9' # Python 3.6 is only officially supported by Django 1.11+. - python: 3.6 env: DJANGO_VERSION='Django>=1.11a1,<2.0' # Django 2.0 only supports Python 3.5+. - python: 3.5 env: DJANGO_VERSION=https://github.com/django/django/archive/master.tar.gz - python: 3.6 env: DJANGO_VERSION=https://github.com/django/django/archive/master.tar.gz - python: 2.7 env: lint install: travis_retry pip install flake8==3.0.4 script: flake8 --show-source allow_failures: # Django master is allowed to fail - env: DJANGO_VERSION=https://github.com/django/django/archive/master.tar.gz fast_finish: true script: python -m unittest discover whitenoise-3.3.1/CHANGELOG.rst000066400000000000000000000001561316151246300157210ustar00rootroot00000000000000Change Log ========== Please see the `documentation `_. whitenoise-3.3.1/LICENSE000066400000000000000000000020661316151246300147070ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2013 David Evans Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. whitenoise-3.3.1/MANIFEST.in000066400000000000000000000002521316151246300154330ustar00rootroot00000000000000include README.rst include LICENSE recursive-include whitenoise * recursive-include docs * recursive-exclude * __pycache__ recursive-exclude * *.py[co] prune docs/_build whitenoise-3.3.1/README.rst000066400000000000000000000032161316151246300153670ustar00rootroot00000000000000WhiteNoise ========== .. image:: https://img.shields.io/travis/evansd/whitenoise.svg :target: https://travis-ci.org/evansd/whitenoise :alt: Build Status .. image:: https://img.shields.io/pypi/v/whitenoise.svg :target: https://pypi.python.org/pypi/whitenoise :alt: Latest PyPI version .. image:: https://img.shields.io/github/stars/evansd/whitenoise.svg?style=social&label=Star :target: https://github.com/evansd/whitenoise :alt: GitHub project **Radically simplified static file serving for Python web apps** With a couple of lines of config WhiteNoise allows your web app to serve its own static files, making it a self-contained unit that can be deployed anywhere without relying on nginx, Amazon S3 or any other external service. (Especially useful on Heroku, OpenShift and other PaaS providers.) It's designed to work nicely with a CDN for high-traffic sites so you don't have to sacrifice performance to benefit from simplicity. WhiteNoise works with any WSGI-compatible app but has some special auto-configuration features for Django. WhiteNoise takes care of best-practices for you, for instance: * Serving compressed content (gzip and Brotli formats, handling Accept-Encoding and Vary headers correctly) * Setting far-future cache headers on content which won't change Worried that serving static files with Python is horribly inefficient? Still think you should be using Amazon S3? Have a look at the `Infrequently Asked Questions`_. To get started, see the documentation_. .. _Infrequently Asked Questions: http://whitenoise.evans.io/en/stable/#infrequently-asked-questions .. _documentation: http://whitenoise.evans.io/en/stable/ whitenoise-3.3.1/docs/000077500000000000000000000000001316151246300146265ustar00rootroot00000000000000whitenoise-3.3.1/docs/Makefile000066400000000000000000000127141316151246300162730ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/WhiteNoise.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/WhiteNoise.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/WhiteNoise" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/WhiteNoise" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." whitenoise-3.3.1/docs/base.rst000066400000000000000000000215351316151246300163000ustar00rootroot00000000000000Using WhiteNoise with any WSGI application ========================================== .. note:: These instructions apply to any WSGI application. However, for Django applications you would be better off using the :doc:`DjangoWhiteNoise ` class which makes integration easier. To enable WhiteNoise you need to wrap your existing WSGI application in a WhiteNoise instance and tell it where to find your static files. For example: .. code-block:: python from whitenoise import WhiteNoise from my_project import MyWSGIApp application = MyWSGIApp() application = WhiteNoise(application, root='/path/to/static/files') application.add_files('/path/to/more/static/files', prefix='more-files/') On initialization, WhiteNoise walks over all the files in the directories that have been added (descending into sub-directories) and builds a list of available static files. Any requests which match a static file get served by WhiteNoise, all others are passed through to the original WSGI application. See the sections on :ref:`compression ` and :ref:`caching ` for further details. WhiteNoise API -------------- .. class:: WhiteNoise(application, root=None, prefix=None, \**kwargs) :param callable application: Original WSGI application :param str root: If set, passed to ``add_files`` method :param str prefix: If set, passed to ``add_files`` method :param \**kwargs: Sets :ref:`configuration attributes ` for this instance .. method:: WhiteNoise.add_files(root, prefix=None, followlinks=False) :param str root: Absolute path to a directory of static files to be served :param str prefix: If set, the URL prefix under which the files will be served. Trailing slashes are automatically added. :param bool followlinks: Whether to follow directory symlinks when walking the directory tree to find files. Note that symlinks to files will always work. .. _compression: Compression Support ------------------- When WhiteNoise builds its list of available files it checks for corresponding files with a ``.gz`` and a ``.br`` suffix (e.g., ``scripts/app.js``, ``scripts/app.js.gz`` and ``scripts/app.js.br``). If it finds them, it will assume that they are (respectively) gzip and `brotli`_ compressed versions of the original file and it will serve them in preference to the uncompressed version where clients indicate that they that compression format (see note on Amazon S3 for why this behaviour is important). .. _cli-utility: WhiteNoise comes with a command line utility which will generate compressed versions of your files for you. Note that in order for brotli compression to work the `brotlipy`_ Python package must be installed. .. _brotli: https://en.wikipedia.org/wiki/Brotli .. _brotlipy: https://brotlipy.readthedocs.io/ Usage is simple: .. code-block:: console $ python -m whitenoise.compress --help usage: compress.py [-h] [-q] [--no-gzip] [--no-brotli] root [extensions [extensions ...]] Search for all files inside *not* matching and produce compressed versions with '.gz' and '.br' suffixes (as long as this results in a smaller file) positional arguments: root Path root from which to search for files extensions File extensions to exclude from compression (default: jpg, jpeg, png, gif, webp, zip, gz, tgz, bz2, tbz, swf, flv, woff, woff2) optional arguments: -h, --help show this help message and exit -q, --quiet Don't produce log output --no-gzip Don't produce gzip '.gz' files --no-brotli Don't produce brotli '.br' files You can either run this during development and commit your compressed files to your repository, or you can run this as part of your build and deploy processes. (Note that DjangoWhiteNoise handles this automatically, if you're using the custom storage backend.) .. _caching: Caching Headers --------------- By default, WhiteNoise sets a max-age header on all responses it sends. You can configure this by passing a ``max_age`` keyword argument. Most modern static asset build systems create uniquely named versions of each file. This results in files which are immutable (i.e., they can never change their contents) and can therefore by cached indefinitely. In order to take advantage of this, WhiteNoise needs to know which files are immutable. This can be done by sub-classing WhiteNoise and overriding the following method: .. code-block:: python def is_immutable_file(self, static_file, url): return False The exact details of how you implement this method will depend on your particular asset build system (see the source for DjangoWhiteNoise for inspiration). Once you have implemented this, any files which are flagged as immutable will have 'cache forever' headers set. Using a Content Distribution Network ------------------------------------ See the instructions for :ref:`using a CDN with Django ` . The same principles apply here although obviously the exact method for generating the URLs for your static files will depend on the libraries you're using. Redirecting to HTTPS -------------------- WhiteNoise does not handle redirection itself, but works well alongside `wsgi-sslify`_, which performs HTTP to HTTPS redirection as well as optionally setting an HSTS header. Simply wrap the WhiteNoise WSGI application with ``sslify()`` - see the `wsgi-sslify`_ documentation for more details. .. _wsgi-sslify: https://github.com/jacobian/wsgi-sslify .. _configuration: Configuration attributes ------------------------ These can be set by passing keyword arguments to the constructor, or by sub-classing WhiteNoise and setting the attributes directly. .. attribute:: autorefresh :default: ``False`` Recheck the filesystem to see if any files have changed before responding. This is designed to be used in development where it can be convenient to pick up changes to static files without restarting the server. For both performance and security reasons, this setting should not be used in production. .. attribute:: max_age :default: ``60`` Time (in seconds) for which browsers and proxies should cache files. The default is chosen to be short enough not to cause problems with stale versions but long enough that, if you're running WhiteNoise behind a CDN, the CDN will still take the majority of the strain during times of heavy load. .. attribute:: mimetypes :default: ``None`` A dictionary mapping file extensions (lowercase) to the mimetype for that extension. For example: :: {'.foo': 'application/x-foo'} Note that WhiteNoise ships with its own default set of mimetypes and does not use the system-supplied ones (e.g. ``/etc/mime.types``). This ensures that it behaves consistently regardless of the environment in which it's run. View the defaults in the :file:`media_types.py ` file. In addition to file extensions, mimetypes can be specified by supplying the entire filename, for example: :: {'some-special-file': 'application/x-custom-type'} .. attribute:: charset :default: ``utf-8`` Charset to add as part of the ``Content-Type`` header for all files whose mimetype allows a charset. .. attribute:: allow_all_origins :default: ``True`` Toggles whether to send an ``Access-Control-Allow-Origin: *`` header for all static files. This allows cross-origin requests for static files which means your static files will continue to work as expected even if they are served via a CDN and therefore on a different domain. Without this your static files will *mostly* work, but you may have problems with fonts loading in Firefox, or accessing images in canvas elements, or other mysterious things. The W3C `explicitly state`__ that this behaviour is safe for publicly accessible files. .. __: http://www.w3.org/TR/cors/#security .. attribute:: add_headers_function :default: ``None`` Reference to a function which is passed the headers object for each static file, allowing it to modify them. For example: :: def force_download_pdfs(headers, path, url): if path.endswith('.pdf'): headers['Content-Disposition'] = 'attachment' application = WhiteNoise(application, add_headers_function=force_download_pdfs) The function is passed: headers A `wsgiref.headers`__ instance (which you can treat just as a dict) containing the headers for the current file path The absolute path to the local file url The host-relative URL of the file e.g. ``/static/styles/app.css`` The function should not return anything; changes should be made by modifying the headers dictionary directly. .. __: https://docs.python.org/3/library/wsgiref.html#module-wsgiref.headers whitenoise-3.3.1/docs/changelog.rst000066400000000000000000000163461316151246300173210ustar00rootroot00000000000000Change Log ========== v3.3.1 ------ * Fix issue with the immutable file test when running behind a CDN which rewrites paths (thanks @lskillen). v3.3.0 ------ * Support the new `immutable `_ Cache-Control header. This gives better caching behaviour for immutable resources than simply setting a large max age. v3.2.3 ------ * Gracefully handle invalid byte sequences in URLs. * Gracefully handle filenames which are too long for the filesystem. * Send correct Content-Type for Adobe's ``crossdomain.xml`` files. v3.2.2 ------ * Convert any config values supplied as byte strings to text to avoid runtime encoding errors when encountering non-ASCII filenames. v3.2.1 ------ * Handle non-ASCII URLs correctly when using the ``wsgi.py`` integration. * Fix exception triggered when a static files "finder" returned a directory rather than a file. v3.2 ---- * Add support for the new-style middleware classes introduced in Django 1.10. The same WhiteNoiseMiddleware class can now be used in either the old ``MIDDLEWARE_CLASSES`` list or the new ``MIDDLEWARE`` list. * Fixed a bug where incorrect Content-Type headers were being sent on 304 Not Modified responses (thanks `@oppianmatt `_). * Return Vary and Cache-Control headers on 304 responses, as specified by the `RFC `_. v3.1 ---- * Add new :any:`WHITENOISE_STATIC_PREFIX` setting to give flexibility in supporting non-standard deployment configurations e.g. serving the application somewhere other than the domain root. * Fix bytes/unicode bug when running with Django 1.10 on Python 2.7 v3.0 ---- .. note:: The latest version of WhiteNoise contains some small **breaking changes**. Most users will be able to upgrade without any problems, but some less-used APIs have been modified: * The setting ``WHITENOISE_GZIP_EXCLUDE_EXTENSIONS`` has been renamed to ``WHITENOISE_SKIP_COMPRESS_EXTENSIONS``. * The CLI :ref:`compression utility ` has moved from ``python -m whitenoise.gzip`` to ``python -m whitenoise.compress``. * The now redundant ``gzipstatic`` management command has been removed. * WhiteNoise no longer uses the system mimetypes files, so if you are serving particularly obscure filetypes you may need to add their mimetypes explicitly using the new :any:`mimetypes ` setting. * Older versions of Django (1.4-1.7) and Python (2.6) are no longer supported. If you need support for these platforms you can continue to use `WhiteNoise 2.x`_. * The ``whitenoise.django.GzipManifestStaticFilesStorage`` storage backend has been moved to ``whitenoise.storage.CompressedManifestStaticFilesStorage``. The old import path **will continue to work** for now, but users are encouraged to update their code to use the new path. .. _WhiteNoise 2.x: http://whitenoise.evans.io/en/legacy-2.x/ Simpler, cleaner Django middleware integration ++++++++++++++++++++++++++++++++++++++++++++++ WhiteNoise can now integrate with Django by adding a single line to ``MIDDLEWARE_CLASSES`` without any need to edit ``wsgi.py``. This also means that WhiteNoise plays nicely with other middleware classes such as *SecurityMiddleware*, and that it is fully compatible with the new `Channels`_ system. See the :ref:`updated documentation ` for details. .. _Channels: https://channels.readthedocs.io/ Brotli compression support ++++++++++++++++++++++++++ `Brotli`_ is the modern, more efficient alternative to gzip for HTTP compression. To benefit from smaller files and faster page loads, just install the `brotlipy`_ library, update your ``requirements.txt`` and WhiteNoise will take care of the rest. See the :ref:`documentation ` for details. .. _brotli: https://en.wikipedia.org/wiki/Brotli .. _brotlipy: https://brotlipy.readthedocs.io/ Simpler customisation +++++++++++++++++++++ It's now possible to add custom headers to WhiteNoise without needing to create a subclass, using the new :any:`add_headers_function ` setting. Use WhiteNoise in development with Django +++++++++++++++++++++++++++++++++++++++++ There's now an option to force Django to use WhiteNoise in development, rather than its own static file handling. This results in more consistent behaviour between development and production environments and fewer opportunities for bugs and surprises. See the :ref:`documentation ` for details. Improved mimetype handling ++++++++++++++++++++++++++ WhiteNoise now ships with its own mimetype definitions (based on those shipped with nginx) instead of relying on the system ones, which can vary between environments. There is a new :any:`mimetypes ` configuration option which makes it easy to add additional type definitions if needed. Thanks ++++++ A big thank-you to `Ed Morley `_ and `Tim Graham `_ for their contributions to this release. --------------------------- v2.0.6 ------ * Rebuild with latest version of `wheel` to get `extras_require` support. v2.0.5 ------ * Add missing argparse dependency for Python 2.6 (thanks @movermeyer)). v2.0.4 ------ * Report path on MissingFileError (thanks @ezheidtmann). v2.0.3 ------ * Add `__version__` attribute. v2.0.2 ------ * More helpful error message when STATIC_URL is set to the root of a domain (thanks @dominicrodger). v2.0.1 ------ * Add support for Python 2.6. * Add a more helpful error message when attempting to import DjangoWhiteNoise before `DJANGO_SETTINGS_MODULE` is defined. v2.0 ------ * Add an `autorefresh` mode which picks up changes to static files made after application startup (for use in development). * Add a `use_finders` mode for DjangoWhiteNoise which finds files in their original directories without needing them collected in `STATIC_ROOT` (for use in development). Note, this is only useful if you don't want to use Django's default runserver behaviour. * Remove the `follow_symlinks` argument from `add_files` and now always follow symlinks. * Support extra mimetypes which Python doesn't know about by default (including .woff2 format) * Some internal refactoring. Note, if you subclass WhiteNoise to add custom behaviour you may need to make some small changes to your code. v1.0.6 ------ * Fix unhelpful exception inside `make_helpful_exception` on Python 3 (thanks @abbottc). v1.0.5 ------ * Fix error when attempting to gzip empty files (thanks @ryanrhee). v1.0.4 ------ * Don't attempt to gzip ``.woff`` files as they're already compressed. * Base decision to gzip on compression ratio achieved, so we don't incur gzip overhead just to save a few bytes. * More helpful error message from ``collectstatic`` if CSS files reference missing assets. v1.0.3 ------ * Fix bug in Last Modified date handling (thanks to Atsushi Odagiri for spotting). v1.0.2 ------ * Set the default max_age parameter in base class to be what the docs claimed it was. v1.0.1 ------ * Fix path-to-URL conversion for Windows. * Remove cruft from packaging manifest. v1.0 ---- * First stable release. whitenoise-3.3.1/docs/conf.py000066400000000000000000000206221316151246300161270ustar00rootroot00000000000000# flake8: noqa # -*- coding: utf-8 -*- # # WhiteNoise documentation build configuration file, created by # sphinx-quickstart on Sun Aug 11 15:22:49 2013. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import datetime, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) def get_version(): import ast, os, re filename = os.path.join( os.path.dirname(__file__), '../whitenoise/__init__.py') with open(filename, 'rb') as f: contents = f.read().decode('utf-8') version_string = re.search(r'__version__\s+=\s+(.*)', contents).group(1) return str(ast.literal_eval(version_string)) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.extlinks'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'WhiteNoise' copyright = u'2013-{}, David Evans'.format(datetime.datetime.today().year) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = get_version() # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. if os.environ.get('READTHEDOCS', None) == 'True': html_theme = 'default' else: import sphinx_rtd_theme html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'WhiteNoisedoc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'WhiteNoise.tex', u'WhiteNoise Documentation', u'David Evans', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'whitenoise', u'WhiteNoise Documentation', [u'David Evans'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'WhiteNoise', u'WhiteNoise Documentation', u'David Evans', 'WhiteNoise', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' git_tag = 'v{}'.format(version) if version != 'development' else 'master' github_base_url = 'https://github.com/evansd/whitenoise/blob/{}/'.format(git_tag) extlinks = {'file': (github_base_url + '%s', '')} whitenoise-3.3.1/docs/django.rst000066400000000000000000000407461316151246300166350ustar00rootroot00000000000000Using WhiteNoise with Django ============================ .. note:: To use WhiteNoise with a non-Django application see the :doc:`generic WSGI documentation `. This guide walks you through setting up a Django project with WhiteNoise. In most cases it shouldn't take more than a couple of lines of configuration. I mention Heroku in a few place as that was the initial use case which prompted me to create WhiteNoise, but there's nothing Heroku-specific about WhiteNoise and the instructions below should apply whatever your hosting platform. 1. Make sure *staticfiles* is configured correctly ---------------------------------------------------- If you're familiar with Django you'll know what to do. If you're just getting started with a new Django project then you'll need add the following to the bottom of your ``settings.py`` file: .. code-block:: python STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') As part of deploying your application you'll need to run ``./manage.py collectstatic`` to put all your static files into ``STATIC_ROOT``. (If you're running on Heroku then this is done automatically for you.) In Django 1.9 and older, make sure you're using the static_ template tag to refer to your static files. For example: .. code-block:: html {% load static from staticfiles %} Hi! In Django 1.10 and later, you can use ``{% load static %}`` instead. .. _static: https://docs.djangoproject.com/en/1.9/ref/contrib/staticfiles/#std:templatetag-staticfiles-static .. _django-middleware: 2. Enable WhiteNoise -------------------- Edit your ``settings.py`` file and add WhiteNoise to the ``MIDDLEWARE_CLASSES`` list, above all other middleware apart from Django's `SecurityMiddleware `_: .. code-block:: python MIDDLEWARE_CLASSES = [ # 'django.middleware.security.SecurityMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware', # ... ] That's it -- WhiteNoise will now serve your static files. However, to get the best performance you should proceed to step 3 below and enable compression and caching. 3. Add compression and caching support -------------------------------------- WhiteNoise comes with a storage backend which automatically takes care of compressing your files and creating unique names for each version so they can safely be cached forever. To use it, just add this to your ``settings.py``: .. code-block:: python STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' If you need to compress files outside of the static files storage system you can use the supplied :ref:`command line utility ` .. note:: If you are having problems after switching to the WhiteNoise storage backend please see the :ref:`troubleshooting guide `. .. _brotli-compression: Brotli compression ++++++++++++++++++ As well as the common gzip compression format, WhiteNoise supports the newer, more efficient `brotli`_ format. This helps reduce bandwidth and increase loading speed. To enable brotli compression you will need the `brotlipy`_ Python package installed, usually by running ``pip install brotlipy`` and updating your ``requirements.txt`` file. Brotli is supported by Firefox and will shortly be available in Chrome, and no doubt other browsers too. WhiteNoise will only serve brotli data to browsers which request it so there are no compatibility issues with enabling brotli support. Also note that browsers will only request brotli data over an HTTPS connection. .. _brotli: https://en.wikipedia.org/wiki/Brotli .. _brotlipy: https://brotlipy.readthedocs.io .. _cdn: 4. Use a Content-Delivery Network --------------------------------- The above steps will get you decent performance on moderate traffic sites, however for higher traffic sites, or sites where performance is a concern you should look at using a CDN. Because WhiteNoise sends appropriate cache headers with your static content, the CDN will be able to cache your files and serve them without needing to contact your application again. Below are instruction for setting up WhiteNoise with Amazon CloudFront, a popular choice of CDN. The process for other CDNs should look very similar though. Instructions for Amazon CloudFront ++++++++++++++++++++++++++++++++++ Go to CloudFront section of the AWS Web Console, and click "Create Distribution". Put your application's domain (without the http prefix) in the "Origin Domain Name" field and leave the rest of the settings as they are. It might take a few minutes for your distribution to become active. Once it's ready, copy the distribution domain name into your ``settings.py`` file so it looks something like this: .. code-block:: python STATIC_HOST = 'https://d4663kmspf1sqa.cloudfront.net' if not DEBUG else '' STATIC_URL = STATIC_HOST + '/static/' Or, even better, you can avoid hardcoding your CDN into your settings by doing something like this: .. code-block:: python STATIC_HOST = os.environ.get('DJANGO_STATIC_HOST', '') STATIC_URL = STATIC_HOST + '/static/' This way you can configure your CDN just by setting an environment variable. For apps on Heroku, you'd run this command .. code-block:: bash heroku config:set DJANGO_STATIC_HOST=https://d4663kmspf1sqa.cloudfront.net .. note:: By default your entire site will be accessible via the CloudFront URL. It's possible that this can cause SEO problems if these URLs start showing up in search results. You can restrict CloudFront to only proxy your static files by following :ref:`these directions `. .. _runserver-nostatic: 5. Using WhiteNoise in development ---------------------------------- In development Django's ``runserver`` automatically takes over static file handling. In most cases this is fine, however this means that some of the improvements that WhiteNoise makes to static file handling won't be available in development and it opens up the possibility for differences in behaviour between development and production environments. For this reason it's a good idea to use WhiteNoise in development as well. You can disable Django's static file handling and allow WhiteNoise to take over simply by passing the ``--nostatic`` option to the ``runserver`` command, but you need to remember to add this option every time you call ``runserver``. An easier way is to edit your ``settings.py`` file and add ``whitenoise.runserver_nostatic`` immediately above ``django.contrib.staticfiles`` like so: .. code-block:: python INSTALLED_APPS = [ # ... 'whitenoise.runserver_nostatic', 'django.contrib.staticfiles', # ... ] Available Settings ------------------ The DjangoWhiteNoise class takes all the same configuration options as the WhiteNoise base class, but rather than accepting keyword arguments to its constructor it uses Django settings. The setting names are just the keyword arguments uppercased with a 'WHITENOISE\_' prefix. .. attribute:: WHITENOISE_ROOT :default: ``None`` Absolute path to a directory of files which will be served at the root of your application (ignored if not set). Don't use this for the bulk of your static files because you won't benefit from cache versioning, but it can be convenient for files like ``robots.txt`` or ``favicon.ico`` which you want to serve at a specific URL. .. attribute:: WHITENOISE_AUTOREFRESH :default: ``settings.DEBUG`` Recheck the filesystem to see if any files have changed before responding. This is designed to be used in development where it can be convenient to pick up changes to static files without restarting the server. For both performance and security reasons, this setting should not be used in production. .. attribute:: WHITENOISE_USE_FINDERS :default: ``settings.DEBUG`` Instead of only picking up files collected into ``STATIC_ROOT``, find and serve files in their original directories using Django's "finders" API. This is the same behaviour as ``runserver`` provides by default, and is only useful if you don't want to use the default ``runserver`` configuration in development. .. attribute:: WHITENOISE_MAX_AGE :default: ``60 if not settings.DEBUG else 0`` Time (in seconds) for which browsers and proxies should cache **non-versioned** files. Versioned files (i.e. files which have been given a unique name like *base.a4ef2389.css* by including a hash of their contents in the name) are detected automatically and set to be cached forever. The default is chosen to be short enough not to cause problems with stale versions but long enough that, if you're running WhiteNoise behind a CDN, the CDN will still take the majority of the strain during times of heavy load. .. attribute:: WHITENOISE_MIMETYPES :default: ``None`` A dictionary mapping file extensions (lowercase) to the mimetype for that extension. For example: :: {'.foo': 'application/x-foo'} Note that WhiteNoise ships with its own default set of mimetypes and does not use the system-supplied ones (e.g. ``/etc/mime.types``). This ensures that it behaves consistently regardless of the environment in which it's run. View the defaults in the :file:`media_types.py ` file. In addition to file extensions, mimetypes can be specified by supplying the entire filename, for example: :: {'some-special-file': 'application/x-custom-type'} .. attribute:: WHITENOISE_CHARSET :default: ``settings.FILE_CHARSET`` (utf-8) Charset to add as part of the ``Content-Type`` header for all files whose mimetype allows a charset. .. attribute:: WHITENOISE_ALLOW_ALL_ORIGINS :default: ``True`` Toggles whether to send an ``Access-Control-Allow-Origin: *`` header for all static files. This allows cross-origin requests for static files which means your static files will continue to work as expected even if they are served via a CDN and therefore on a different domain. Without this your static files will *mostly* work, but you may have problems with fonts loading in Firefox, or accessing images in canvas elements, or other mysterious things. The W3C `explicitly state`__ that this behaviour is safe for publicly accessible files. .. __: http://www.w3.org/TR/cors/#security .. attribute:: WHITENOISE_SKIP_COMPRESS_EXTENSIONS :default: ``('jpg', 'jpeg', 'png', 'gif', 'webp','zip', 'gz', 'tgz', 'bz2', 'tbz', 'swf', 'flv', 'woff')`` File extensions to skip when compressing. Because the compression process will only create compressed files where this results in an actual size saving, it would be safe to leave this list empty and attempt to compress all files. However, for files which we're confident won't benefit from compression, it speeds up the process if we just skip over them. .. attribute:: WHITENOISE_ADD_HEADERS_FUNCTION :default: ``None`` Reference to a function which is passed the headers object for each static file, allowing it to modify them. For example: :: def force_download_pdfs(headers, path, url): if path.endswith('.pdf'): headers['Content-Disposition'] = 'attachment' WHITENOISE_ADD_HEADERS_FUNCTION = force_download_pdfs The function is passed: headers A `wsgiref.headers`__ instance (which you can treat just as a dict) containing the headers for the current file path The absolute path to the local file url The host-relative URL of the file e.g. ``/static/styles/app.css`` The function should not return anything; changes should be made by modifying the headers dictionary directly. .. __: https://docs.python.org/3/library/wsgiref.html#module-wsgiref.headers .. attribute:: WHITENOISE_STATIC_PREFIX :default: Path component of ``settings.STATIC_URL`` The URL prefix under which static files will be served. Usually this can be determined automatically by using the path component of ``STATIC_URL``. So if ``STATIC_URL`` is ``https://example.com/static/`` then ``WHITENOISE_STATIC_PREFIX`` will be ``/static/``. However there are cases where it's useful to set these independently, for instance if the application is not running at the root of the domain or if your CDN is doing path rewriting. Additional Notes ---------------- Django Compressor +++++++++++++++++ For performance and security reasons WhiteNoise does not check for new files after startup (unless using Django `DEBUG` mode). As such, all static files must be generated in advance. If you're using Django Compressor, this can be performed using its `offline compression`_ feature. .. _offline compression: https://django-compressor.readthedocs.io/en/latest/usage/#offline-compression -------------------------------------------------------------------------- Serving Media Files +++++++++++++++++++ WhiteNoise is not suitable for serving user-uploaded "media" files. For one thing, as described above, it only checks for static files at startup and so files added after the app starts won't be seen. More importantly though, serving user-uploaded files from the same domain as your main application is a security risk (this `blog post`_ from Google security describes the problem well). And in addition to that, using local disk to store and serve your user media makes it harder to scale your application across multiple machines. For all these reasons, it's much better to store files on a separate dedicated storage service and serve them to users from there. The `django-storages`_ library provides many options e.g. Amazon S3, Azure Storage, and Rackspace CloudFiles. .. _blog post: https://security.googleblog.com/2012/08/content-hosting-for-modern-web.html .. _django-storages: https://django-storages.readthedocs.io/ -------------------------------------------------------------------------- .. _storage-troubleshoot: Troubleshooting the WhiteNoise Storage backend ++++++++++++++++++++++++++++++++++++++++++++++ If you're having problems with the WhiteNoise storage backend, the chances are they're due to the underlying Django storage engine. This is because WhiteNoise only adds a thin wrapper around Django's storage to add compression support, and because the compression code is very simple it generally doesn't cause problems. The most common issue is that there are CSS files which reference other files (usually images or fonts) which don't exist at that specified path. When Django attempts to rewrite these references it looks for the corresponding file and throws an error if it can't find it. To test whether the problems are due to WhiteNoise or not, try swapping the WhiteNoise storage backend for the Django one: .. code-block:: python STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage' If the problems persist then your issue is with Django itself (try the docs_ or the `mailing list`_). If the problem only occurs with WhiteNoise then raise a ticket on the `issue tracker`_. .. _docs: https://docs.djangoproject.com/en/stable/ref/contrib/staticfiles/ .. _mailing list: https://groups.google.com/d/forum/django-users .. _issue tracker: https://github.com/evansd/whitenoise/issues -------------------------------------------------------------------------- .. _restricting-cloudfront: Restricting CloudFront to static files ++++++++++++++++++++++++++++++++++++++ The instructions for setting up CloudFront given above will result in the entire site being accessible via the CloudFront URL. It's possible that this can cause SEO problems if these URLs start showing up in search results. You can restrict CloudFront to only proxy your static files by following these directions: 1. Go to your newly created distribution and click "*Distribution Settings*", then the "*Behaviors*" tab, then "*Create Behavior*". Put ``static/*`` into the path pattern and click "*Create*" to save. 2. Now select the ``Default (*)`` behaviour and click "*Edit*". Set "*Restrict Viewer Access*" to "*Yes*" and then click "*Yes, Edit*" to save. 3. Check that the ``static/*`` pattern is first on the list, and the default one is second. This will ensure that requests for static files are passed through but all others are blocked. Using other storage backends ++++++++++++++++++++++++++++ WhiteNoise will only work with storage backends that stores their files on the local filesystem in ``STATIC_ROOT``. It will not work with backends that store files remotely, for instance on Amazon S3. whitenoise-3.3.1/docs/flask.rst000066400000000000000000000047001316151246300164610ustar00rootroot00000000000000Using WhiteNoise with Flask ============================ This guide walks you through setting up a Flask project with WhiteNoise. In most cases it shouldn't take more than a couple of lines of configuration. 1. Make sure where your *static* is located ------------------------------------------- If you're familiar with Flask you'll know what to do. If you're just getting started with a new Flask project then the default is the ``static`` folder in the root path of the application. Check the ``static_folder`` argument in `Flask Application Object documentation `_ for further information. 2. Enable WhiteNoise -------------------- In the file where you create your app you instantiate Flask Application Object (the ``flask.Flask()`` object). All you have to do is to wrap it with ``WhiteNoise()`` object. If you use Flask quick start approach it will look something like that: .. code-block:: python from flask import Flask from whitenoise import WhiteNoise app = WhiteNoise(Flask(__name__), root='static/') If you opt for the `pattern of creating your app with a function `_, then it would look like that: .. code-block:: python from flask import Flask from sqlalchemy import create_engine from whitenoise import WhiteNoise from myapp import config from myapp.views import frontend def create_app(database_uri, debug=False): app = Flask(__name__) app.debug = debug # set up your database app.engine = create_engine(database_uri) # register your blueprints app.register_blueprint(frontend) # add whitenoise app.wsgi_app = WhiteNoise(app.wsgi_app, root='static/') # other setup tasks return app That's it -- WhiteNoise will now serve your static files. 3. Custom *static* folder ------------------------- If it turns out that your are not using the Flask default for *static* folder, fear not. You can instantiate WhiteNoise and add your *static* folders later: .. code-block:: python from flask import Flask from whitenoise import WhiteNoise app = WhiteNoise(Flask(__name__)) my_static_folders = ( 'static/folder/one/', 'static/folder/two/', 'static/folder/three/' ) for static in my_static_folders: app.add_files(static) And check ``WhiteNoise.add_file`` documentation for further customization. whitenoise-3.3.1/docs/index.rst000066400000000000000000000177001316151246300164740ustar00rootroot00000000000000WhiteNoise ========== .. image:: https://img.shields.io/travis/evansd/whitenoise.svg :target: https://travis-ci.org/evansd/whitenoise :alt: Build Status .. image:: https://img.shields.io/pypi/v/whitenoise.svg :target: https://pypi.python.org/pypi/whitenoise :alt: Latest PyPI version .. image:: https://img.shields.io/github/stars/evansd/whitenoise.svg?style=social&label=Star :target: https://github.com/evansd/whitenoise :alt: GitHub project **Radically simplified static file serving for Python web apps** With a couple of lines of config WhiteNoise allows your web app to serve its own static files, making it a self-contained unit that can be deployed anywhere without relying on nginx, Amazon S3 or any other external service. (Especially useful on Heroku, OpenShift and other PaaS providers.) It's designed to work nicely with a CDN for high-traffic sites so you don't have to sacrifice performance to benefit from simplicity. WhiteNoise works with any WSGI-compatible app but has some special auto-configuration features for Django. WhiteNoise takes care of best-practices for you, for instance: * Serving compressed content (gzip and Brotli formats, handling Accept-Encoding and Vary headers correctly) * Setting far-future cache headers on content which won't change Worried that serving static files with Python is horribly inefficient? Still think you should be using Amazon S3? Have a look at the `Infrequently Asked Questions`_ below. QuickStart for Django apps -------------------------- Edit your ``settings.py`` file and add WhiteNoise to the ``MIDDLEWARE_CLASSES`` list, above all other middleware apart from Django's `SecurityMiddleware `_: .. code-block:: python MIDDLEWARE_CLASSES = [ # 'django.middleware.security.SecurityMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware', # ... ] That's it, you're ready to go. Want forever-cacheable files and compression support? Just add this to your ``settings.py``: .. code-block:: python STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' For more details, including on setting up CloudFront and other CDNs see the :doc:`Using WhiteNoise with Django ` guide. QuickStart for other WSGI apps ------------------------------ To enable WhiteNoise you need to wrap your existing WSGI application in a WhiteNoise instance and tell it where to find your static files. For example: .. code-block:: python from whitenoise import WhiteNoise from my_project import MyWSGIApp application = MyWSGIApp() application = WhiteNoise(application, root='/path/to/static/files') application.add_files('/path/to/more/static/files', prefix='more-files/') And that's it, you're ready to go. For more details see the :doc:`full documentation `. Using WhiteNoise with Flask --------------------------- WhiteNoise was not specifically written with Flask in mind, but as Flask uses the standard WSGI protocol it is easy to integrate with WhiteNoise (see the :doc:`Using WhiteNoise with Flask ` guide). Compatibility ------------- WhiteNoise works with any WSGI-compatible application and is tested on Python **2.7**, **3.3**, **3.4**, **3.5** and **PyPy**. DjangoWhiteNoise is tested with Django versions **1.8** --- **1.10** Endorsements ------------ WhiteNoise owes its initial popularity to the nice things that some of Django and pip's core developers said about it: `@jezdez `_: *[WhiteNoise] is really awesome and should be the standard for Django + Heroku* `@dstufft `_: *WhiteNoise looks pretty excellent.* `@idangazit `_ *Received a positive brainsmack from @_EvansD's WhiteNoise. Vastly smarter than S3 for static assets. What was I thinking before?* It's now being used by thousands of projects, including some high-profile sites such as `mozilla.org `_. Issues & Contributing --------------------- Raise an issue on the `GitHub project `_ or feel free to nudge `@_EvansD `_ on Twitter. Infrequently Asked Questions ---------------------------- Isn't serving static files from Python horribly inefficient? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ The short answer to this is that if you care about performance and efficiency then you should be using WhiteNoise behind a CDN like CloudFront. If you're doing *that* then, because of the caching headers WhiteNoise sends, the vast majority of static requests will be served directly by the CDN without touching your application, so it really doesn't make much difference how efficient WhiteNoise is. That said, WhiteNoise is pretty efficient. Because it only has to serve a fixed set of files it does all the work of finding files and determining the correct headers upfront on initialization. Requests can then be served with little more than a dictionary lookup to find the appropriate response. Also, when used with gunicorn (and most other WSGI servers) the actual business of pushing the file down the network interface is handled by the kernel's very efficient ``sendfile`` syscall, not by Python. Shouldn't I be pushing my static files to S3 using something like Django-Storages? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ No, you shouldn't. The main problem with this approach is that Amazon S3 cannot currently selectively serve compressed content to your users. Compression (using either the venerable gzip or the more modern brotli algorithms) can make dramatic reductions in the bandwidth required for your CSS and JavaScript. But in order to do this correctly the server needs to examine the ``Accept-Encoding`` header of the request to determine which compression formats are supported, and return an appropriate ``Vary`` header so that intermediate caches know to do the same. This is exactly what WhiteNoise does, but Amazon S3 currently provides no means of doing this. The second problem with a push-based approach to handling static files is that it adds complexity and fragility to your deployment process: extra libraries specific to your storage backend, extra configuration and authentication keys, and extra tasks that must be run at specific points in the deployment in order for everything to work. With the CDN-as-caching-proxy approach that WhiteNoise takes there are just two bits of configuration: your application needs the URL of the CDN, and the CDN needs the URL of your application. Everything else is just standard HTTP semantics. This makes your deployments simpler, your life easier, and you happier. What's the point in WhiteNoise when I can do the same thing in a few lines of Apache/nginx config? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ There are two answers here. One is that WhiteNoise is designed to work in situations were Apache, nginx and the like aren't easily available. But more importantly, it's easy to underestimate what's involved in serving static files correctly. Does your few lines of nginx config distinguish between files which might change and files which will never change and set the cache headers appropriately? Did you add the right CORS headers so that your fonts load correctly when served via a CDN? Did you turn on the special nginx setting which allows it to send gzipped content in response to an ``HTTP/1.0`` request, which for some reason CloudFront still uses? Did you install the extension which allows you to serve pre-compressed brotli-encoded content to modern browsers? None of this is rocket science, but it's fiddly and annoying and WhiteNoise takes care of all it for you. License ------- MIT Licensed .. toctree:: :hidden: self django base flask changelog whitenoise-3.3.1/scripts/000077500000000000000000000000001316151246300153655ustar00rootroot00000000000000whitenoise-3.3.1/scripts/generate_default_media_types.py000077500000000000000000000031101316151246300236160ustar00rootroot00000000000000#!/usr/bin/env python import pprint EXTRA_MIMETYPES = { 'apple-app-site-association': 'application/pkc7-mime', '.woff': 'application/font-woff', '.woff2': 'font/woff2' } FUNCTION_TEMPLATE = """ def default_types(): {triple_quote} We use our own set of default media types rather than the system-supplied ones. This ensures consistent media type behaviour across varied environments. The defaults are based on those shipped with nginx, with some custom additions. {triple_quote} return {{ {types_map} }} """.lstrip() NGINX_CONFIG_FILE = '/etc/nginx/mime.types' def get_default_types_function(): types_map = get_types_map() types_map_str = pprint.pformat(types_map, indent=8).strip('{} ') return FUNCTION_TEMPLATE.format( triple_quote='"""', types_map=types_map_str) def get_types_map(): types_map = {} with open(NGINX_CONFIG_FILE, 'r') as f: for line in f: line = line.strip() if not line.endswith(';'): continue line = line.rstrip(';') bits = line.split() media_type = bits[0] # This is the default media type anyway, no point specifying # it explicitly if media_type == 'application/octet-stream': continue extensions = bits[1:] for extension in extensions: types_map['.'+extension] = media_type types_map.update(EXTRA_MIMETYPES) return types_map if __name__ == '__main__': print get_default_types_function() whitenoise-3.3.1/setup.cfg000066400000000000000000000000661316151246300155210ustar00rootroot00000000000000[wheel] universal = 1 [flake8] max-line-length = 100 whitenoise-3.3.1/setup.py000066400000000000000000000033651316151246300154170ustar00rootroot00000000000000import ast import codecs import os import re from setuptools import setup, find_packages PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__)) VERSION_RE = re.compile(r'__version__\s+=\s+(.*)') def read(*path): full_path = os.path.join(PROJECT_ROOT, *path) with codecs.open(full_path, 'r', encoding='utf-8') as f: return f.read() version_string = VERSION_RE.search(read('whitenoise/__init__.py')).group(1) version = str(ast.literal_eval(version_string)) setup( name='whitenoise', version=version, author='David Evans', author_email='d@evans.io', url='http://whitenoise.evans.io', packages=find_packages(exclude=['tests*']), license='MIT', description="Radically simplified static file serving for WSGI applications", long_description=read('README.rst'), classifiers=[ 'Development Status :: 5 - Production/Stable', 'Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware', 'Framework :: Django', 'Framework :: Django :: 1.8', 'Framework :: Django :: 1.9', 'Framework :: Django :: 1.10', # Temporarily removed pending resolution of # https://github.com/pypa/warehouse/issues/1673 # 'Framework :: Django :: 1.11', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', ], ) whitenoise-3.3.1/tests/000077500000000000000000000000001316151246300150405ustar00rootroot00000000000000whitenoise-3.3.1/tests/__init__.py000066400000000000000000000000001316151246300171370ustar00rootroot00000000000000whitenoise-3.3.1/tests/django_settings.py000066400000000000000000000012371316151246300205770ustar00rootroot00000000000000SECRET_KEY = '4' STATIC_URL = '/static/' ROOT_URLCONF = 'tests.django_urls' INSTALLED_APPS = ( 'django.contrib.staticfiles', 'whitenoise', ) ALLOWED_HOSTS = ['*'] LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'filters': { 'require_debug_false': { '()': 'django.utils.log.RequireDebugFalse' } }, 'handlers': { 'log_to_stderr': { 'level': 'ERROR', 'class': 'logging.StreamHandler' } }, 'loggers': { 'django.request': { 'handlers': ['log_to_stderr'], 'level': 'ERROR', 'propagate': True, }, } } whitenoise-3.3.1/tests/django_urls.py000066400000000000000000000003401316151246300177160ustar00rootroot00000000000000from django.conf.urls import url from django.http import HttpResponse def hello_world(reqeust): return HttpResponse(content='Hello Word', content_type='text/plain') urlpatterns = [ url(r'^hello$', hello_world), ] whitenoise-3.3.1/tests/test_compress.py000066400000000000000000000034671316151246300203160ustar00rootroot00000000000000from __future__ import unicode_literals import contextlib import errno import gzip import os import shutil import tempfile from unittest import TestCase from whitenoise.compress import main as compress_main COMPRESSABLE_FILE = 'application.css' TOO_SMALL_FILE = 'too-small.css' WRONG_EXTENSION = 'image.jpg' TEST_FILES = { COMPRESSABLE_FILE: b'a' * 1000, TOO_SMALL_FILE: b'hi', } class CompressTestBase(TestCase): @classmethod def setUpClass(cls): # Make a temporary directory and copy in test files cls.tmp = tempfile.mkdtemp() for path, contents in TEST_FILES.items(): path = os.path.join(cls.tmp, path.lstrip('/')) try: os.makedirs(os.path.dirname(path)) except OSError as e: if e.errno != errno.EEXIST: raise with open(path, 'wb') as f: f.write(contents) cls.run_compress() super(CompressTestBase, cls).setUpClass() @classmethod def tearDownClass(cls): super(CompressTestBase, cls).tearDownClass() # Remove temporary directory shutil.rmtree(cls.tmp) class CompressTest(CompressTestBase): @classmethod def run_compress(cls): compress_main(cls.tmp, quiet=True) def test_compresses_file(self): with contextlib.closing( gzip.open( os.path.join(self.tmp, COMPRESSABLE_FILE + '.gz'), 'rb')) as f: contents = f.read() self.assertEqual(TEST_FILES[COMPRESSABLE_FILE], contents) def test_doesnt_compress_if_no_saving(self): self.assertFalse(os.path.exists(os.path.join(self.tmp, TOO_SMALL_FILE + 'gz'))) def test_ignores_other_extensions(self): self.assertFalse(os.path.exists(os.path.join(self.tmp, WRONG_EXTENSION + '.gz'))) whitenoise-3.3.1/tests/test_django_whitenoise.py000066400000000000000000000125651316151246300221620ustar00rootroot00000000000000from __future__ import unicode_literals import shutil import tempfile import django from django.test import SimpleTestCase from django.test.utils import override_settings from django.conf import settings from django.contrib.staticfiles import storage, finders from django.core.wsgi import get_wsgi_application from django.core.management import call_command from django.utils.functional import empty from .utils import TestServer, Files from whitenoise.django import DjangoWhiteNoise django.setup() def reset_lazy_object(obj): obj._wrapped = empty @override_settings() class DjangoWhiteNoiseTest(SimpleTestCase): @classmethod def setUpClass(cls): cls.static_files = Files('static', css='styles.css', nonascii='nonascii\u2713.txt') cls.root_files = Files('root', robots='robots.txt') cls.tmp = tempfile.mkdtemp() settings.STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' settings.STATICFILES_DIRS = [cls.static_files.directory] settings.STATIC_ROOT = cls.tmp settings.WHITENOISE_ROOT = cls.root_files.directory # Collect static files into STATIC_ROOT call_command('collectstatic', verbosity=0, interactive=False) # Initialize test application cls.application = cls.init_application() cls.server = TestServer(cls.application) super(DjangoWhiteNoiseTest, cls).setUpClass() @classmethod def init_application(cls): django_app = get_wsgi_application() return DjangoWhiteNoise(django_app) @classmethod def tearDownClass(cls): super(DjangoWhiteNoiseTest, cls).tearDownClass() reset_lazy_object(storage.staticfiles_storage) # Remove temporary directory shutil.rmtree(cls.tmp) def test_get_root_file(self): response = self.server.get(self.root_files.robots_url) self.assertEqual(response.content, self.root_files.robots_content) def test_versioned_file_cached_forever(self): url = storage.staticfiles_storage.url(self.static_files.css_path) response = self.server.get(url) self.assertEqual(response.content, self.static_files.css_content) self.assertEqual(response.headers.get('Cache-Control'), 'max-age={}, public, immutable'.format(DjangoWhiteNoise.FOREVER)) def test_unversioned_file_not_cached_forever(self): url = settings.STATIC_URL + self.static_files.css_path response = self.server.get(url) self.assertEqual(response.content, self.static_files.css_content) self.assertEqual(response.headers.get('Cache-Control'), 'max-age={}, public'.format(DjangoWhiteNoise.max_age)) def test_get_gzip(self): url = storage.staticfiles_storage.url(self.static_files.css_path) response = self.server.get(url) self.assertEqual(response.content, self.static_files.css_content) self.assertEqual(response.headers['Content-Encoding'], 'gzip') self.assertEqual(response.headers['Vary'], 'Accept-Encoding') def test_no_content_type_when_not_modified(self): last_mod = 'Fri, 11 Apr 2100 11:47:06 GMT' url = settings.STATIC_URL + self.static_files.css_path response = self.server.get(url, headers={'If-Modified-Since': last_mod}) self.assertNotIn('Content-Type', response.headers) def test_get_nonascii_file(self): url = settings.STATIC_URL + self.static_files.nonascii_path response = self.server.get(url) self.assertEqual(response.content, self.static_files.nonascii_content) @override_settings() class UseFindersTest(SimpleTestCase): @classmethod def setUpClass(cls): cls.static_files = Files('static', css='styles.css') settings.STATICFILES_DIRS = [cls.static_files.directory] settings.WHITENOISE_USE_FINDERS = True settings.WHITENOISE_AUTOREFRESH = True # Clear cache to pick up new settings try: finders.get_finder.cache_clear() except AttributeError: finders._finders.clear() # Initialize test application cls.application = cls.init_application() cls.server = TestServer(cls.application) super(UseFindersTest, cls).setUpClass() @classmethod def init_application(cls): django_app = get_wsgi_application() return DjangoWhiteNoise(django_app) def test_get_file_from_static_dir(self): url = settings.STATIC_URL + self.static_files.css_path response = self.server.get(url) self.assertEqual(response.content, self.static_files.css_content) def test_non_ascii_requests_safely_ignored(self): response = self.server.get(u"/\u263A") self.assertEqual(404, response.status_code) def test_requests_for_directory_safely_ignored(self): url = settings.STATIC_URL + 'directory' response = self.server.get(url) self.assertEqual(404, response.status_code) class DjangoMiddlewareTest(DjangoWhiteNoiseTest): @classmethod def init_application(cls): if django.VERSION >= (1, 10): setting_name = 'MIDDLEWARE' else: setting_name = 'MIDDLEWARE_CLASSES' middleware = list(getattr(settings, setting_name) or []) middleware.insert(0, 'whitenoise.middleware.WhiteNoiseMiddleware') setattr(settings, setting_name, middleware) return get_wsgi_application() whitenoise-3.3.1/tests/test_files/000077500000000000000000000000001316151246300172015ustar00rootroot00000000000000whitenoise-3.3.1/tests/test_files/assets/000077500000000000000000000000001316151246300205035ustar00rootroot00000000000000whitenoise-3.3.1/tests/test_files/assets/compressed.css000066400000000000000000000003321316151246300233570ustar00rootroot00000000000000body { background-color: white; color: black; } .thing { border: 1px solid black; color: blue; } .thing2 { border: 1px solid green; color: red; } .thing3 { border: 1px solid yellow; color: purple; } whitenoise-3.3.1/tests/test_files/assets/compressed.css.gz000066400000000000000000000002001316151246300237700ustar00rootroot00000000000000‹“Ņ£VuĶA ƒ0Š}N1PØīźigˆĮ!¦•Ņ»l…Łøüü’ų^p‡šÆq*%a3 ‹>aāBC­žŁs īė\»L1…EŖå#ošŽxĶ +dTwƂ%ÔШžFķÄ,«a¹hęóļ‰GÉŚwhitenoise-3.3.1/tests/test_files/assets/custom-mime.foobar000066400000000000000000000000111316151246300241240ustar00rootroot00000000000000fizzbuzz whitenoise-3.3.1/tests/test_files/assets/subdir/000077500000000000000000000000001316151246300217735ustar00rootroot00000000000000whitenoise-3.3.1/tests/test_files/assets/subdir/javascript.js000066400000000000000000000000431316151246300244740ustar00rootroot00000000000000var myFunction = { return 42; }; whitenoise-3.3.1/tests/test_files/root/000077500000000000000000000000001316151246300201645ustar00rootroot00000000000000whitenoise-3.3.1/tests/test_files/root/robots.txt000066400000000000000000000000141316151246300222300ustar00rootroot00000000000000Disallow: / whitenoise-3.3.1/tests/test_files/static/000077500000000000000000000000001316151246300204705ustar00rootroot00000000000000whitenoise-3.3.1/tests/test_files/static/directory/000077500000000000000000000000001316151246300224745ustar00rootroot00000000000000whitenoise-3.3.1/tests/test_files/static/directory/.keep000066400000000000000000000000001316151246300234070ustar00rootroot00000000000000whitenoise-3.3.1/tests/test_files/static/nonasciiāœ“.txt000066400000000000000000000000031316151246300240460ustar00rootroot00000000000000hi whitenoise-3.3.1/tests/test_files/static/styles.css000066400000000000000000000003321316151246300225230ustar00rootroot00000000000000body { background-color: white; color: black; } .thing { border: 1px solid black; color: blue; } .thing2 { border: 1px solid green; color: red; } .thing3 { border: 1px solid yellow; color: purple; } whitenoise-3.3.1/tests/test_storage.py000066400000000000000000000015611316151246300201200ustar00rootroot00000000000000from django.contrib.staticfiles.storage import HashedFilesMixin from django.test import SimpleTestCase from django.test.utils import override_settings from whitenoise.storage import HelpfulExceptionMixin, MissingFileError @override_settings() class DjangoWhiteNoiseStorageTest(SimpleTestCase): def test_make_helpful_exception(self): class TriggerException(HashedFilesMixin): def exists(self, path): return False exception = None try: TriggerException().hashed_name('/missing/file.png') except ValueError as e: exception = e helpful_exception = HelpfulExceptionMixin().make_helpful_exception( exception, 'styles/app.css' ) self.assertIsInstance(helpful_exception, MissingFileError) whitenoise-3.3.1/tests/test_whitenoise.py000066400000000000000000000142561316151246300206370ustar00rootroot00000000000000import os import tempfile from unittest import TestCase import shutil from wsgiref.simple_server import demo_app from .utils import TestServer, Files from whitenoise import WhiteNoise # Update Py2 TestCase to support Py3 method names if not hasattr(TestCase, 'assertRegex'): class Py3TestCase(TestCase): def assertRegex(self, *args, **kwargs): return self.assertRegexpMatches(*args, **kwargs) TestCase = Py3TestCase class WhiteNoiseTest(TestCase): @classmethod def setUpClass(cls): cls.files = cls.init_files() cls.application = cls.init_application(root=cls.files.directory) cls.server = TestServer(cls.application) super(WhiteNoiseTest, cls).setUpClass() @staticmethod def init_files(): return Files('assets', js='subdir/javascript.js', gzip='compressed.css', gzipped='compressed.css.gz', custom_mime='custom-mime.foobar') @staticmethod def init_application(**kwargs): def custom_headers(headers, path, url): if url.endswith('.css'): headers['X-Is-Css-File'] = 'True' kwargs.update(max_age=1000, mimetypes={'.foobar': 'application/x-foo-bar'}, add_headers_function=custom_headers) return WhiteNoise(demo_app, **kwargs) def test_get_file(self): response = self.server.get(self.files.js_url) self.assertEqual(response.content, self.files.js_content) self.assertRegex(response.headers['Content-Type'], r'application/javascript\b') self.assertRegex(response.headers['Content-Type'], r'.*\bcharset="utf-8"') def test_get_not_accept_gzip(self): response = self.server.get(self.files.gzip_url, headers={'Accept-Encoding': ''}) self.assertEqual(response.content, self.files.gzip_content) self.assertEqual(response.headers.get('Content-Encoding', ''), '') self.assertEqual(response.headers['Vary'], 'Accept-Encoding') def test_get_accept_gzip(self): response = self.server.get(self.files.gzip_url) self.assertEqual(response.content, self.files.gzip_content) self.assertEqual(response.headers['Content-Encoding'], 'gzip') self.assertEqual(response.headers['Vary'], 'Accept-Encoding') def test_not_modified_exact(self): response = self.server.get(self.files.js_url) last_mod = response.headers['Last-Modified'] response = self.server.get(self.files.js_url, headers={'If-Modified-Since': last_mod}) self.assertEqual(response.status_code, 304) def test_not_modified_future(self): last_mod = 'Fri, 11 Apr 2100 11:47:06 GMT' response = self.server.get(self.files.js_url, headers={'If-Modified-Since': last_mod}) self.assertEqual(response.status_code, 304) def test_modified(self): last_mod = 'Fri, 11 Apr 2001 11:47:06 GMT' response = self.server.get(self.files.js_url, headers={'If-Modified-Since': last_mod}) self.assertEqual(response.status_code, 200) def test_max_age(self): response = self.server.get(self.files.js_url) self.assertEqual(response.headers['Cache-Control'], 'max-age=1000, public') def test_other_requests_passed_through(self): response = self.server.get('/not/static') self.assertIn('Hello world!', response.text) def test_non_ascii_requests_safely_ignored(self): response = self.server.get(u"/\u263A") self.assertIn('Hello world!', response.text) def test_add_under_prefix(self): prefix = '/prefix' self.application.add_files(self.files.directory, prefix=prefix) response = self.server.get(prefix + self.files.js_url) self.assertEqual(response.content, self.files.js_content) def test_response_has_allow_origin_header(self): response = self.server.get(self.files.js_url) self.assertEqual(response.headers.get('Access-Control-Allow-Origin'), '*') def test_response_has_correct_content_length_header(self): response = self.server.get(self.files.js_url) length = int(response.headers['Content-Length']) self.assertEqual(length, len(self.files.js_content)) def test_gzip_response_has_correct_content_length_header(self): response = self.server.get(self.files.gzip_url) length = int(response.headers['Content-Length']) self.assertEqual(length, len(self.files.gzipped_content)) def test_post_request_returns_405(self): response = self.server.request('post', self.files.js_url) self.assertEqual(response.status_code, 405) def test_head_request_has_no_body(self): response = self.server.request('head', self.files.js_url) self.assertEqual(response.status_code, 200) self.assertFalse(response.content) def test_custom_mimetype(self): response = self.server.get(self.files.custom_mime_url) self.assertRegex(response.headers['Content-Type'], r'application/x-foo-bar\b') def test_custom_headers(self): response = self.server.get(self.files.gzip_url) self.assertEqual(response.headers['x-is-css-file'], 'True') class WhiteNoiseAutorefresh(WhiteNoiseTest): @classmethod def setUpClass(cls): cls.files = cls.init_files() cls.tmp = tempfile.mkdtemp() cls.application = cls.init_application(root=cls.tmp, autorefresh=True) cls.server = TestServer(cls.application) # Copy in the files *after* initializing server copytree(cls.files.directory, cls.tmp) super(WhiteNoiseTest, cls).setUpClass() def test_no_error_on_very_long_filename(self): response = self.server.get('/blah' * 1000) self.assertNotEqual(response.status_code, 500) @classmethod def tearDownClass(cls): super(WhiteNoiseTest, cls).tearDownClass() # Remove temporary directory shutil.rmtree(cls.tmp) def copytree(src, dst): for name in os.listdir(src): src_path = os.path.join(src, name) dst_path = os.path.join(dst, name) if os.path.isdir(src_path): shutil.copytree(src_path, dst_path) else: shutil.copy2(src_path, dst_path) whitenoise-3.3.1/tests/utils.py000066400000000000000000000031771316151246300165620ustar00rootroot00000000000000from __future__ import unicode_literals import os import threading import warnings from wsgiref.simple_server import make_server, WSGIRequestHandler import requests warnings.filterwarnings(action='ignore', category=DeprecationWarning, module='requests') TEST_FILE_PATH = os.path.join(os.path.dirname(__file__), 'test_files') class SilentWSGIHandler(WSGIRequestHandler): def log_message(*args): pass class TestServer(object): """ Wraps a WSGI application and allows you to make real HTTP requests against it """ def __init__(self, application): self.application = application self.server = make_server('127.0.0.1', 0, application, handler_class=SilentWSGIHandler) def get(self, *args, **kwargs): return self.request('get', *args, **kwargs) def request(self, method, path, *args, **kwargs): url = 'http://{0[0]}:{0[1]}{1}'.format(self.server.server_address, path) thread = threading.Thread(target=self.server.handle_request) thread.start() response = requests.request(method, url, *args, **kwargs) thread.join() return response class Files(object): def __init__(self, directory, **files): self.directory = os.path.join(TEST_FILE_PATH, directory) for name, path in files.items(): url = '/' + path with open(os.path.join(self.directory, path), 'rb') as f: content = f.read() setattr(self, name + '_path', path) setattr(self, name + '_url', url) setattr(self, name + '_content', content) whitenoise-3.3.1/tox.ini000066400000000000000000000014241316151246300152120ustar00rootroot00000000000000[tox] envlist = py27-lint, py{27,34,35,py}-django{18,19,110,111}, py33-django18, py36-django111, py{35,36}-master, [testenv] basepython = py27: python2.7 py33: python3.3 py34: python3.4 py35: python3.5 py36: python3.6 pypy: pypy commands = coverage run --branch --include=whitenoise/* -m unittest discover coverage report setenv = PYTHONWARNINGS = all DJANGO_SETTINGS_MODULE = tests.django_settings COVERAGE_FILE = .coverage.{envname} deps = coverage>=4.2,<4.3 requests>=2.11,<2.12 django18: Django>=1.8,<1.9 django19: Django>=1.9,<1.10 django110: Django>=1.10,<1.11 django111: Django>=1.11a1,<2.0 djangomaster: https://github.com/django/django/archive/master.tar.gz [testenv:py27-lint] commands = flake8 --show-source deps = flake8==3.0.4 whitenoise-3.3.1/whitenoise/000077500000000000000000000000001316151246300160545ustar00rootroot00000000000000whitenoise-3.3.1/whitenoise/__init__.py000066400000000000000000000001161316151246300201630ustar00rootroot00000000000000from .base import WhiteNoise __version__ = '3.3.1' __all__ = ['WhiteNoise'] whitenoise-3.3.1/whitenoise/base.py000066400000000000000000000151021316151246300173370ustar00rootroot00000000000000from email.utils import formatdate import os from posixpath import normpath from wsgiref.headers import Headers from wsgiref.util import FileWrapper from .media_types import MediaTypes from .static_file import StaticFile from .utils import (decode_if_byte_string, decode_path_info, ensure_leading_trailing_slash, MissingFileError, stat_regular_file) class WhiteNoise(object): # Ten years is what nginx sets a max age if you use 'expires max;' # so we'll follow its lead FOREVER = 10*365*24*60*60 # Attributes that can be set by keyword args in the constructor config_attrs = ('autorefresh', 'max_age', 'allow_all_origins', 'charset', 'mimetypes', 'add_headers_function') # Re-check the filesystem on every request so that any changes are # automatically picked up. NOTE: For use in development only, not supported # in production autorefresh = False max_age = 60 # Set 'Access-Control-Allow-Orign: *' header on all files. # As these are all public static files this is safe (See # http://www.w3.org/TR/cors/#security) and ensures that things (e.g # webfonts in Firefox) still work as expected when your static files are # served from a CDN, rather than your primary domain. allow_all_origins = True charset = 'utf-8' # Custom mime types mimetypes = None # Callback for adding custom logic when setting headers add_headers_function = None def __init__(self, application, root=None, prefix=None, **kwargs): for attr in self.config_attrs: try: value = kwargs.pop(attr) except KeyError: pass else: value = decode_if_byte_string(value) setattr(self, attr, value) if kwargs: raise TypeError("Unexpected keyword argument '{0}'".format( list(kwargs.keys())[0])) self.media_types = MediaTypes(extra_types=self.mimetypes) self.application = application self.files = {} self.directories = [] if root is not None: self.add_files(root, prefix) def __call__(self, environ, start_response): path = decode_path_info(environ['PATH_INFO']) if self.autorefresh: static_file = self.find_file(path) else: static_file = self.files.get(path) if static_file is None: return self.application(environ, start_response) else: return self.serve(static_file, environ, start_response) def serve(self, static_file, environ, start_response): response = static_file.get_response(environ['REQUEST_METHOD'], environ) status_line = '{} {}'.format(response.status, response.status.phrase) start_response(status_line, list(response.headers)) if response.file is not None: file_wrapper = environ.get('wsgi.file_wrapper', FileWrapper) return file_wrapper(response.file) else: return [] def add_files(self, root, prefix=None): root = decode_if_byte_string(root) prefix = decode_if_byte_string(prefix) prefix = ensure_leading_trailing_slash(prefix) if self.autorefresh: # Later calls to `add_files` overwrite earlier ones, hence we need # to store the list of directories in reverse order so later ones # match first when they're checked in "autorefresh" mode self.directories.insert(0, (root, prefix)) else: self.update_files_dictionary(root, prefix) def update_files_dictionary(self, root, prefix): for directory, _, filenames in os.walk(root, followlinks=True): for filename in filenames: path = os.path.join(directory, filename) url = prefix + os.path.relpath(path, root).replace('\\', '/') self.files[url] = self.get_static_file(path, url) def find_file(self, url): # Don't bother checking URLs which could only ever be directories if not url or url[-1] == '/': return # Attempt to mitigate path traversal attacks. Not sure if this is # sufficient, hence the warning that "autorefresh" is a development # only feature and not for production use if normpath(url) != url: return for root, prefix in self.directories: if url.startswith(prefix): path = os.path.join(root, url[len(prefix):]) try: return self.get_static_file(path, url) except MissingFileError: pass def get_static_file(self, path, url): headers = Headers([]) self.add_stat_headers(headers, path, url) self.add_mime_headers(headers, path, url) self.add_cache_headers(headers, path, url) self.add_cors_headers(headers, path, url) self.add_extra_headers(headers, path, url) if self.add_headers_function: self.add_headers_function(headers, path, url) return StaticFile(path, headers) def add_stat_headers(self, headers, path, url): file_stat = stat_regular_file(path) headers['Last-Modified'] = formatdate(file_stat.st_mtime, usegmt=True) headers['Content-Length'] = str(file_stat.st_size) def add_mime_headers(self, headers, path, url): media_type = self.media_types.get_type(path) charset = self.get_charset(media_type, path, url) params = {'charset': str(charset)} if charset else {} headers.add_header('Content-Type', str(media_type), **params) def get_charset(self, media_type, path, url): if (media_type.startswith('text/') or media_type == 'application/javascript'): return self.charset def add_cache_headers(self, headers, path, url): if self.is_immutable_file(path, url): headers['Cache-Control'] = \ 'max-age={0}, public, immutable'.format(self.FOREVER) elif self.max_age is not None: headers['Cache-Control'] = \ 'max-age={0}, public'.format(self.max_age) def is_immutable_file(self, path, url): """ This should be implemented by sub-classes (see e.g. DjangoWhiteNoise) """ return False def add_cors_headers(self, headers, path, url): if self.allow_all_origins: headers['Access-Control-Allow-Origin'] = '*' def add_extra_headers(self, headers, path, url): """ This is provided as a hook for sub-classes, by default a no-op """ pass whitenoise-3.3.1/whitenoise/compress.py000066400000000000000000000113551316151246300202660ustar00rootroot00000000000000from __future__ import print_function, division, unicode_literals import gzip import os import re try: from io import BytesIO except ImportError: from cStringIO import StringIO as BytesIO try: import brotli brotli_installed = True except ImportError: brotli_installed = False class Compressor(object): # Extensions that it's not worth trying to compress SKIP_COMPRESS_EXTENSIONS = ( # Images 'jpg', 'jpeg', 'png', 'gif', 'webp', # Compressed files 'zip', 'gz', 'tgz', 'bz2', 'tbz', # Flash 'swf', 'flv', # Fonts 'woff', 'woff2') def __init__(self, extensions=None, use_gzip=True, use_brotli=True, log=print, quiet=False): if extensions is None: extensions = self.SKIP_COMPRESS_EXTENSIONS self.extension_re = self.get_extension_re(extensions) self.use_gzip = use_gzip self.use_brotli = use_brotli and brotli_installed if not quiet: self.log = log @staticmethod def get_extension_re(extensions): if not extensions: return re.compile('^$') else: return re.compile( r'\.({0})$'.format('|'.join(map(re.escape, extensions))), re.IGNORECASE) def should_compress(self, filename): return not self.extension_re.search(filename) def log(self, message): pass def compress(self, path): with open(path, 'rb') as f: data = f.read() size = len(data) if self.use_brotli: compressed = self.compress_brotli(data) success = self.write_data(compressed, size, path, '.br', 'Brotli') # If Brotli compression wasn't effective gzip won't be either if not success: return if self.use_gzip: compressed = self.compress_gzip(data) self.write_data(compressed, size, path, '.gz', 'Gzip') @staticmethod def compress_gzip(data): output = BytesIO() # Explicitly set mtime to 0 so gzip content is fully determined # by file content (0 = "no timestamp" according to gzip spec) with gzip.GzipFile(filename='', mode='wb', fileobj=output, compresslevel=9, mtime=0) as gz_file: gz_file.write(data) return output.getvalue() @staticmethod def compress_brotli(data): return brotli.compress(data) def write_data(self, data, orig_size, path, suffix, compression): compressed_size = len(data) if not self.compressed_effectively(orig_size, compressed_size): self.log('Skipping {0} ({1} compression not effective)'.format( path, compression)) return False else: self.log('{0} compressed {1} ({2}K -> {3}K)'.format( compression, path, orig_size // 1024, compressed_size // 1024)) with open(path + suffix, 'wb') as f: f.write(data) return True @staticmethod def compressed_effectively(orig_size, compressed_size): if orig_size == 0: return False ratio = compressed_size / orig_size return ratio <= 0.95 def main(root, **kwargs): compressor = Compressor(**kwargs) for dirpath, dirs, files in os.walk(root): for filename in files: if compressor.should_compress(filename): path = os.path.join(dirpath, filename) compressor.compress(path) if __name__ == '__main__': import argparse parser = argparse.ArgumentParser( description="Search for all files inside *not* matching " " and produce compressed versions with " "'.gz' and '.br' suffixes (as long as this results in a " "smaller file)") parser.add_argument('-q', '--quiet', help="Don't produce log output", action='store_true') parser.add_argument('--no-gzip', help="Don't produce gzip '.gz' files", action='store_false', dest='use_gzip') parser.add_argument('--no-brotli', help="Don't produce brotli '.br' files", action='store_false', dest='use_brotli') parser.add_argument('root', help='Path root from which to search for files') parser.add_argument('extensions', nargs='*', help='File extensions to exclude from compression ' '(default: {})'.format(', '.join( Compressor.SKIP_COMPRESS_EXTENSIONS)), default=Compressor.SKIP_COMPRESS_EXTENSIONS) args = parser.parse_args() main(**vars(args)) whitenoise-3.3.1/whitenoise/django.py000066400000000000000000000111121316151246300176640ustar00rootroot00000000000000from __future__ import absolute_import import os from posixpath import basename from django.conf import settings from django.core.exceptions import ImproperlyConfigured try: from django.contrib.staticfiles.storage import staticfiles_storage except ImproperlyConfigured: if not os.environ.get('DJANGO_SETTINGS_MODULE'): raise ImproperlyConfigured( "'DJANGO_SETTINGS_MODULE' environment variable must be set " "before importing 'whitenoise.django'") else: raise from django.contrib.staticfiles import finders from django.utils.six.moves.urllib.parse import urlparse from .base import WhiteNoise # Import here under an alias for backwards compatibility from .storage import (CompressedManifestStaticFilesStorage as GzipManifestStaticFilesStorage) from .utils import (decode_if_byte_string, ensure_leading_trailing_slash, IsDirectoryError) __all__ = ['DjangoWhiteNoise', 'GzipManifestStaticFilesStorage'] class DjangoWhiteNoise(WhiteNoise): config_attrs = WhiteNoise.config_attrs + ( 'root', 'use_finders', 'static_prefix') root = None use_finders = False static_prefix = None def __init__(self, application, settings=settings): self.configure_from_settings(settings) self.check_settings(settings) super(DjangoWhiteNoise, self).__init__(application) if self.static_root: self.add_files(self.static_root, prefix=self.static_prefix) if self.root: self.add_files(self.root) def configure_from_settings(self, settings): # Default configuration self.charset = settings.FILE_CHARSET self.autorefresh = settings.DEBUG self.use_finders = settings.DEBUG self.static_prefix = urlparse(settings.STATIC_URL or '').path if settings.DEBUG: self.max_age = 0 # Allow settings to override default attributes for attr in self.config_attrs: settings_key = 'WHITENOISE_{0}'.format(attr.upper()) try: value = getattr(settings, settings_key) except AttributeError: pass else: value = decode_if_byte_string(value) setattr(self, attr, value) self.static_prefix = ensure_leading_trailing_slash(self.static_prefix) self.static_root = decode_if_byte_string(settings.STATIC_ROOT) def check_settings(self, settings): if self.use_finders and not self.autorefresh: raise ImproperlyConfigured( 'WHITENOISE_USE_FINDERS can only be enabled in development ' 'when WHITENOISE_AUTOREFRESH is also enabled.' ) def find_file(self, url): if self.use_finders and url.startswith(self.static_prefix): path = finders.find(url[len(self.static_prefix):]) if path: try: return self.get_static_file(path, url) except IsDirectoryError: return None return super(DjangoWhiteNoise, self).find_file(url) def is_immutable_file(self, path, url): """ Determine whether given URL represents an immutable file (i.e. a file with a hash of its contents as part of its name) which can therefore be cached forever """ if not url.startswith(self.static_prefix): return False name = url[len(self.static_prefix):] name_without_hash = self.get_name_without_hash(name) if name == name_without_hash: return False static_url = self.get_static_url(name_without_hash) # If the static_url function maps the name without hash # back to the original name, then we know we've got a # versioned filename if static_url and basename(static_url) == basename(url): return True return False def get_name_without_hash(self, filename): """ Removes the version hash from a filename e.g, transforms 'css/application.f3ea4bcc2.css' into 'css/application.css' Note: this is specific to the naming scheme used by Django's CachedStaticFilesStorage. You may have to override this if you are using a different static files versioning system """ name_with_hash, ext = os.path.splitext(filename) name = os.path.splitext(name_with_hash)[0] return name + ext def get_static_url(self, name): try: return decode_if_byte_string(staticfiles_storage.url(name)) except ValueError: return None whitenoise-3.3.1/whitenoise/httpstatus_backport.py000066400000000000000000000010311316151246300225310ustar00rootroot00000000000000""" Very partial backport of the `http.HTTPStatus` enum from Python 3.5 This implements just enough of the interface for our purposes, it does not attempt to be a full implementation. """ class HTTPStatus(int): phrase = None def __new__(cls, code, phrase): instance = int.__new__(cls, code) instance.phrase = phrase return instance HTTPStatus.OK = HTTPStatus(200, 'OK') HTTPStatus.NOT_MODIFIED = HTTPStatus(304, 'Not Modified') HTTPStatus.METHOD_NOT_ALLOWED = HTTPStatus(405, 'Method Not Allowed') whitenoise-3.3.1/whitenoise/media_types.py000066400000000000000000000113001316151246300207240ustar00rootroot00000000000000import os class MediaTypes(object): def __init__(self, default='application/octet-stream', extra_types=None): self.types_map = default_types() self.default = default if extra_types: self.types_map.update(extra_types) def get_type(self, path): name = os.path.basename(path).lower() media_type = self.types_map.get(name) if media_type is not None: return media_type extension = os.path.splitext(name)[1] return self.types_map.get(extension, self.default) def default_types(): """ We use our own set of default media types rather than the system-supplied ones. This ensures consistent media type behaviour across varied environments. The defaults are based on those shipped with nginx, with some custom additions. """ return { '.3gp': 'video/3gpp', '.3gpp': 'video/3gpp', '.7z': 'application/x-7z-compressed', '.ai': 'application/postscript', '.asf': 'video/x-ms-asf', '.asx': 'video/x-ms-asf', '.atom': 'application/atom+xml', '.avi': 'video/x-msvideo', '.bmp': 'image/x-ms-bmp', '.cco': 'application/x-cocoa', '.crt': 'application/x-x509-ca-cert', '.css': 'text/css', '.der': 'application/x-x509-ca-cert', '.doc': 'application/msword', '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', '.ear': 'application/java-archive', '.eot': 'application/vnd.ms-fontobject', '.eps': 'application/postscript', '.flv': 'video/x-flv', '.gif': 'image/gif', '.hqx': 'application/mac-binhex40', '.htc': 'text/x-component', '.htm': 'text/html', '.html': 'text/html', '.ico': 'image/x-icon', '.jad': 'text/vnd.sun.j2me.app-descriptor', '.jar': 'application/java-archive', '.jardiff': 'application/x-java-archive-diff', '.jng': 'image/x-jng', '.jnlp': 'application/x-java-jnlp-file', '.jpeg': 'image/jpeg', '.jpg': 'image/jpeg', '.js': 'application/javascript', '.json': 'application/json', '.kar': 'audio/midi', '.kml': 'application/vnd.google-earth.kml+xml', '.kmz': 'application/vnd.google-earth.kmz', '.m3u8': 'application/vnd.apple.mpegurl', '.m4a': 'audio/x-m4a', '.m4v': 'video/x-m4v', '.mid': 'audio/midi', '.midi': 'audio/midi', '.mml': 'text/mathml', '.mng': 'video/x-mng', '.mov': 'video/quicktime', '.mp3': 'audio/mpeg', '.mp4': 'video/mp4', '.mpeg': 'video/mpeg', '.mpg': 'video/mpeg', '.ogg': 'audio/ogg', '.pdb': 'application/x-pilot', '.pdf': 'application/pdf', '.pem': 'application/x-x509-ca-cert', '.pl': 'application/x-perl', '.pm': 'application/x-perl', '.png': 'image/png', '.ppt': 'application/vnd.ms-powerpoint', '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', '.prc': 'application/x-pilot', '.ps': 'application/postscript', '.ra': 'audio/x-realaudio', '.rar': 'application/x-rar-compressed', '.rpm': 'application/x-redhat-package-manager', '.rss': 'application/rss+xml', '.rtf': 'application/rtf', '.run': 'application/x-makeself', '.sea': 'application/x-sea', '.shtml': 'text/html', '.sit': 'application/x-stuffit', '.svg': 'image/svg+xml', '.svgz': 'image/svg+xml', '.swf': 'application/x-shockwave-flash', '.tcl': 'application/x-tcl', '.tif': 'image/tiff', '.tiff': 'image/tiff', '.tk': 'application/x-tcl', '.ts': 'video/mp2t', '.txt': 'text/plain', '.war': 'application/java-archive', '.wbmp': 'image/vnd.wap.wbmp', '.webm': 'video/webm', '.webp': 'image/webp', '.wml': 'text/vnd.wap.wml', '.wmlc': 'application/vnd.wap.wmlc', '.wmv': 'video/x-ms-wmv', '.woff': 'application/font-woff', '.woff2': 'font/woff2', '.xhtml': 'application/xhtml+xml', '.xls': 'application/vnd.ms-excel', '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', '.xml': 'text/xml', '.xpi': 'application/x-xpinstall', '.xspf': 'application/xspf+xml', '.zip': 'application/zip', 'apple-app-site-association': 'application/pkc7-mime', # Adobe Products - see: # https://www.adobe.com/devnet-docs/acrobatetk/tools/AppSec/xdomain.html#policy-file-host-basics 'crossdomain.xml': 'text/x-cross-domain-policy' } whitenoise-3.3.1/whitenoise/middleware.py000066400000000000000000000027351316151246300205520ustar00rootroot00000000000000from __future__ import absolute_import from django.http import FileResponse from whitenoise.django import DjangoWhiteNoise class WhiteNoiseMiddleware(DjangoWhiteNoise): """ Wrap DjangoWhiteNoise to allow it to function as Django middleware, rather than WSGI middleware This functions as both old- and new-style middleware, so can be included in either MIDDLEWARE or MIDDLEWARE_CLASSES. """ def __init__(self, get_response=None): self.get_response = get_response # We pass None for `application` super(WhiteNoiseMiddleware, self).__init__(None) def __call__(self, request): response = self.process_request(request) if response is None: response = self.get_response(request) return response def process_request(self, request): if self.autorefresh: static_file = self.find_file(request.path_info) else: static_file = self.files.get(request.path_info) if static_file is not None: return self.serve(static_file, request) def serve(self, static_file, request): response = static_file.get_response(request.method, request.META) status = int(response.status) http_response = FileResponse(response.file or (), status=status) # Remove default content-type del http_response['content-type'] for key, value in response.headers: http_response[key] = value return http_response whitenoise-3.3.1/whitenoise/runserver_nostatic/000077500000000000000000000000001316151246300220135ustar00rootroot00000000000000whitenoise-3.3.1/whitenoise/runserver_nostatic/__init__.py000066400000000000000000000000001316151246300241120ustar00rootroot00000000000000whitenoise-3.3.1/whitenoise/runserver_nostatic/management/000077500000000000000000000000001316151246300241275ustar00rootroot00000000000000whitenoise-3.3.1/whitenoise/runserver_nostatic/management/__init__.py000066400000000000000000000000001316151246300262260ustar00rootroot00000000000000whitenoise-3.3.1/whitenoise/runserver_nostatic/management/commands/000077500000000000000000000000001316151246300257305ustar00rootroot00000000000000whitenoise-3.3.1/whitenoise/runserver_nostatic/management/commands/__init__.py000066400000000000000000000000001316151246300300270ustar00rootroot00000000000000whitenoise-3.3.1/whitenoise/runserver_nostatic/management/commands/runserver.py000066400000000000000000000004031316151246300303320ustar00rootroot00000000000000""" This file exists solely to shadow the `runserver` command provided by `django.contrib.staticfiles` and restore the original `runserver` behaviour """ from django.core.management.commands.runserver import Command # Keep flake8 happy __all__ = ['Command'] whitenoise-3.3.1/whitenoise/static_file.py000066400000000000000000000071161316151246300207210ustar00rootroot00000000000000from collections import namedtuple from email.utils import parsedate try: from http import HTTPStatus except ImportError: from .httpstatus_backport import HTTPStatus import re from wsgiref.headers import Headers from .utils import MissingFileError, stat_regular_file Response = namedtuple('Response', ('status', 'headers', 'file')) NOT_ALLOWED = Response(HTTPStatus.METHOD_NOT_ALLOWED, (('Allow', 'GET, HEAD'),), None) ACCEPT_GZIP_RE = re.compile(r'\bgzip\b') ACCEPT_BROTLI_RE = re.compile(r'\bbr\b') # Headers which should be returned with a 304 Not Modified response as # specified here: http://tools.ietf.org/html/rfc7232#section-4.1 NOT_MODIFIED_HEADERS = ('Cache-Control', 'Content-Location', 'Date', 'ETag', 'Expires', 'Vary') NOT_MODIFIED_HEADER_RE = re.compile('^({})$'.format( '|'.join(map(re.escape, NOT_MODIFIED_HEADERS))), re.IGNORECASE) class StaticFile(object): def __init__(self, path, headers): plain_file, gzip_file, brotli_file = get_alternatives(path, headers) self.plain_file = file_tuple(plain_file) self.gzip_file = file_tuple(gzip_file) self.brotli_file = file_tuple(brotli_file) self.last_modified = parsedate(headers['Last-Modified']) self.not_modified_response = get_not_modified_response(self.plain_file[1]) def get_response(self, method, request_headers): if method != 'GET' and method != 'HEAD': return NOT_ALLOWED elif self.file_not_modified(request_headers): return self.not_modified_response path, headers = self.get_path_and_headers(request_headers) if method != 'HEAD': file_handle = open(path, 'rb') else: file_handle = None return Response(HTTPStatus.OK, headers, file_handle) def get_path_and_headers(self, request_headers): accept_encoding = request_headers.get('HTTP_ACCEPT_ENCODING', '') if self.brotli_file and ACCEPT_BROTLI_RE.search(accept_encoding): return self.brotli_file if self.gzip_file and ACCEPT_GZIP_RE.search(accept_encoding): return self.gzip_file return self.plain_file def file_not_modified(self, request_headers): try: last_requested = request_headers['HTTP_IF_MODIFIED_SINCE'] except KeyError: return False return parsedate(last_requested) >= self.last_modified def get_alternatives(path, headers): gzip_file = get_alternative_encoding(path, headers, '.gz', 'gzip') brotli_file = get_alternative_encoding(path, headers, '.br', 'br') if gzip_file or brotli_file: headers['Vary'] = 'Accept-Encoding' plain_file = (path, headers) return plain_file, gzip_file, brotli_file def get_alternative_encoding(path, headers, suffix, encoding): alt_path = path + suffix try: alt_size = stat_regular_file(alt_path).st_size except MissingFileError: return None alt_headers = Headers(headers.items()) alt_headers['Vary'] = 'Accept-Encoding' alt_headers['Content-Encoding'] = encoding alt_headers['Content-Length'] = str(alt_size) return alt_path, alt_headers def file_tuple(path_and_headers): if path_and_headers is None: return None else: path, headers = path_and_headers return path, tuple(headers.items()) def get_not_modified_response(headers): not_modified_headers = tuple([ item for item in headers if NOT_MODIFIED_HEADER_RE.match(item[0])]) return Response(HTTPStatus.NOT_MODIFIED, not_modified_headers, None) whitenoise-3.3.1/whitenoise/storage.py000066400000000000000000000064731316151246300201040ustar00rootroot00000000000000from __future__ import absolute_import import os import re import textwrap from django.conf import settings from django.contrib.staticfiles.storage import ManifestStaticFilesStorage from .compress import Compressor class CompressedStaticFilesMixin(object): """ Wraps a StaticFilesStorage instance to create compressed versions of its output files """ def post_process(self, *args, **kwargs): files = super(CompressedStaticFilesMixin, self).post_process(*args, **kwargs) if not kwargs.get('dry_run'): files = self.post_process_with_compression(files) return files def post_process_with_compression(self, files): extensions = getattr(settings, 'WHITENOISE_SKIP_COMPRESS_EXTENSIONS', None) compressor = Compressor(extensions=extensions, quiet=True) for name, hashed_name, processed in files: if self.should_compress(compressor, name, processed): compressor.compress(self.path(name)) if hashed_name is not None: compressor.compress(self.path(hashed_name)) yield name, hashed_name, processed def should_compress(self, compressor, name, processed): if isinstance(processed, Exception): return False else: return compressor.should_compress(name) class HelpfulExceptionMixin(object): """ If a CSS file contains references to images, fonts etc that can't be found then Django's `post_process` blows up with a not particularly helpful ValueError that leads people to think WhiteNoise is broken. Here we attempt to intercept such errors and reformat them to be more helpful in revealing the source of the problem. """ ERROR_MSG_RE = re.compile("^The file '(.+)' could not be found") ERROR_MSG = textwrap.dedent(u"""\ {orig_message} The {ext} file '{filename}' references a file which could not be found: {missing} Please check the URL references in this {ext} file, particularly any relative paths which might be pointing to the wrong location. """) def post_process(self, *args, **kwargs): files = super(HelpfulExceptionMixin, self).post_process(*args, **kwargs) for name, hashed_name, processed in files: if isinstance(processed, Exception): processed = self.make_helpful_exception(processed, name) yield name, hashed_name, processed def make_helpful_exception(self, exception, name): if isinstance(exception, ValueError): message = exception.args[0] if len(exception.args) else '' # Stringly typed exceptions. Yay! match = self.ERROR_MSG_RE.search(message) if match: extension = os.path.splitext(name)[1].lstrip('.').upper() message = self.ERROR_MSG.format( orig_message=message, filename=name, missing=match.group(1), ext=extension) exception = MissingFileError(message) return exception class MissingFileError(ValueError): pass class CompressedManifestStaticFilesStorage( HelpfulExceptionMixin, CompressedStaticFilesMixin, ManifestStaticFilesStorage): pass whitenoise-3.3.1/whitenoise/utils.py000066400000000000000000000031021316151246300175620ustar00rootroot00000000000000import errno import os import stat import sys if sys.version_info[0] >= 3: BINARY_TYPE = bytes else: BINARY_TYPE = str class NotARegularFileError(Exception): pass class MissingFileError(NotARegularFileError): pass class IsDirectoryError(MissingFileError): pass def decode_if_byte_string(s): if isinstance(s, BINARY_TYPE): s = s.decode('utf-8') return s # Follow Django in treating URLs as UTF-8 encoded (which requires undoing the # implicit ISO-8859-1 decoding applied in Python 3). Strictly speaking, URLs # should only be ASCII anyway, but UTF-8 can be found in the wild. if sys.version_info[0] >= 3: def decode_path_info(path_info): return path_info.encode('iso-8859-1', 'replace').decode('utf-8', 'replace') else: def decode_path_info(path_info): return path_info.decode('utf-8', 'replace') def stat_regular_file(path): """ Wrap os.stat to raise appropriate errors if `path` is not a regular file """ try: file_stat = os.stat(path) except OSError as e: if e.errno in (errno.ENOENT, errno.ENAMETOOLONG): raise MissingFileError(path) else: raise if not stat.S_ISREG(file_stat.st_mode): if stat.S_ISDIR(file_stat.st_mode): raise IsDirectoryError(u'Path is a directory: {0}'.format(path)) else: raise NotARegularFileError(u'Not a regular file: {0}'.format(path)) return file_stat def ensure_leading_trailing_slash(path): path = (path or u'').strip(u'/') return u'/{0}/'.format(path) if path else u'/'