├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── README.rst ├── rednoise ├── __init__.py └── base.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | __pycache__ 3 | /MANIFEST 4 | /dist 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Ryan McGrath (2015). Copyright (c) for any originating Whitenoise 4 | code is held by David Evans (2013). 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | recursive-exclude * __pycache__ 4 | recursive-exclude * *.py[co] 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notice 2 | If you've used Django Rednoise before, thanks! I'm glad it was helpful. Unfortunately, this project was built for Whitenoise < 2, and I've simply never found the time to update it, hence why I'm archiving it. If you'd like to, feel free to fork it and take ownership of it. :) 3 | 4 | RedNoise 5 | ========== 6 | Django as a framework is great, but file handling within any setup has never been particularly fun to configure. **[WhiteNoise](https://whitenoise.readthedocs.org/)** makes this, to borrow its own term, "radically simplified", and for the most part I've found it to be an ideal solution - though there are a few things I found myself wanting from time to time. 7 | 8 | RedNoise is a different take on the DjangoWhiteNoise module from WhiteNoise. It aims to be (and as of writing, should be) completely compatible with the existing WhiteNoise API, but takes a different approach on a few things. I consider this an opinionated third-party addon to the WhiteNoise project, however I hope it goes without saying that anything here is up for grabs as a pull request or merge. 9 | 10 | Getting Started 11 | ==================== 12 | 1. `pip install django-rednoise` 13 | 2. Follow the WhiteNoise configuration guidelines - they all should work. 14 | 3. Modify your wsgi file as follows: 15 | 16 | ``` python 17 | from django.core.wsgi import get_wsgi_application 18 | from rednoise import DjangoRedNoise 19 | 20 | application = get_wsgi_application() 21 | application = DjangoRedNoise(application) 22 | ``` 23 | 24 | ...and that's it. You can read on for additional configuration options if you think you need them, but the defaults are more or less sane. DjangoRedNoise is the only Class in this package; existing guides/documentation for WhiteNoise should still suffice. 25 | 26 | Differences from WhiteNoise 27 | ----------------------------------- 28 | 29 | - **RedNoise allows you to serve user-uploaded media** 30 | Note that it performs no gzipping of content or anything; the use case this satisfied (for me, at least) was that users within a CMS 31 | needed to be able to upload images as part of a site; configuring storages and some S3 setup just got annoying to deal with. 32 | 33 | - **RedNoise respects Django's DEBUG flag** 34 | When DEBUG is True, RedNoise will mimic the native Django static files handling routine. With this change, RedNoise can be used while 35 | in development (provided you're developing with uwsgi) so your environment can simulate a production server. I've found this to be 36 | faster than using Django's static serving in urls.py solution, YMMV. 37 | 38 | - **When DEBUG is false, RedNoise mimics WhiteNoise's original behavior** 39 | ...with two exceptions. One, being that Media can also be served, and two - whereas WhiteNoise scans all static files on startup, 40 | RedNoise will look for the file upon user request. If found, it will cache it much like WhiteNoise does - the advantage of this 41 | approach is that one can add static file(s) as necessary after the fact without requiring a restart of the process. 42 | 43 | - **RedNoise 404s directly at the uwsgi level, rather than through the Django application** 44 | Personally speaking, I don't see why Django should bother processing a 404 for an image that we know doesn't exist. This is, of 45 | course, a personal opinion of mine. 46 | 47 | 48 | License 49 | ------- 50 | 51 | MIT Licensed 52 | 53 | Contact 54 | ------- 55 | Questions, concerns? ryan [at] venodesigns dot net 56 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | RedNoise 2 | ========== 3 | Django as a framework is great, but file handling within any setup has never been particularly fun to configure. WhiteNoise (https://whitenoise.readthedocs.org/) makes this, to borrow its own term, "radically simplified", and for the most part I've found it to be an ideal solution - though there are a few things I found myself wanting from time to time. 4 | 5 | RedNoise is a different take on the DjangoWhiteNoise module from WhiteNoise. It aims to be (and as of writing, should be) completely compatible with the existing WhiteNoise API, but takes a different approach on a few things. I consider this an opinionated third-party addon to the WhiteNoise project, however I hope it goes without saying that anything here is up for grabs as a pull request or merge. 6 | 7 | Getting Started 8 | ==================== 9 | 1. ``pip install django-rednoise`` 10 | 2. Follow the WhiteNoise configuration guidelines - they all should work. 11 | 3. Modify your wsgi file as follows: 12 | 13 | .. code-block:: python 14 | 15 | from django.core.wsgi import get_wsgi_application 16 | from rednoise import DjangoRedNoise 17 | 18 | application = get_wsgi_application() 19 | application = DjangoRedNoise(application) 20 | 21 | ...and that's it. You can read on for additional configuration options if you think you need them, but the defaults are more or less sane. DjangoRedNoise is the only Class in this package; existing guides/documentation for WhiteNoise should still suffice. 22 | 23 | Differences from WhiteNoise 24 | ----------------------------------- 25 | 26 | - **RedNoise allows you to serve user-uploaded media** 27 | Note that it performs no gzipping of content or anything; the use case this satisfied (for me, at least) was that users within a CMS 28 | needed to be able to upload images as part of a site; configuring storages and some S3 setup just got annoying to deal with. 29 | 30 | - **RedNoise respects Django's DEBUG flag** 31 | When DEBUG is True, RedNoise will mimic the native Django static files handling routine. With this change, RedNoise can be used while 32 | in development (provided you're developing with uwsgi) so your environment can simulate a production server. I've found this to be 33 | faster than using Django's static serving in urls.py solution, YMMV. 34 | 35 | - **When DEBUG is false, RedNoise mimics WhiteNoise's original behavior** 36 | ...with two exceptions. One, being that Media can also be served, and two - whereas WhiteNoise scans all static files on startup, 37 | RedNoise will look for the file upon user request. If found, it will cache it much like WhiteNoise does - the advantage of this 38 | approach is that one can add static file(s) as necessary after the fact without requiring a restart of the process. 39 | 40 | - **RedNoise 404s directly at the uwsgi level, rather than through the Django application** 41 | Personally speaking, I don't see why Django should bother processing a 404 for an image that we know doesn't exist. This is, of 42 | course, a personal opinion of mine. 43 | 44 | 45 | License 46 | ------- 47 | 48 | MIT Licensed 49 | 50 | Contact 51 | ------- 52 | Questions, concerns? ryan [at] venodesigns dot net 53 | -------------------------------------------------------------------------------- /rednoise/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from .base import DjangoRedNoise 3 | 4 | __all__ = ['DjangoRedNoise'] 5 | -------------------------------------------------------------------------------- /rednoise/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from os.path import isfile 4 | 5 | try: 6 | import urlparse 7 | except ImportError: 8 | import urllib.parse as urlparse 9 | 10 | from django.conf import settings 11 | from django.core.exceptions import ImproperlyConfigured 12 | from django.contrib.staticfiles import finders 13 | 14 | from whitenoise.django import DjangoWhiteNoise 15 | 16 | A404 = '404 NOT FOUND' 17 | A301 = '301 Moved Permanently' 18 | PI = 'PATH_INFO' 19 | 20 | 21 | class DjangoRedNoise(DjangoWhiteNoise): 22 | rednoise_config_attrs = [ 23 | 'should_serve_static', 24 | 'should_serve_media', 25 | 'root_aliases' 26 | ] 27 | 28 | root_aliases = [ 29 | '/favicon.ico', '/sitemap.xml', 30 | '/robots.txt', '/humans.txt' 31 | ] 32 | 33 | debug = False 34 | should_serve_static = True 35 | should_serve_media = True 36 | 37 | def __init__(self, application): 38 | """Basic init stuff. We allow overriding a few extra things. 39 | """ 40 | self.charset = settings.FILE_CHARSET 41 | self.application = application 42 | self.staticfiles_dirs = [] 43 | self.static_files = {} 44 | self.media_files = {} 45 | 46 | # These are commonly used assets that get requested from the root; 47 | # we catch them and redirect to the prefixed static url in production 48 | # or just serve them in development. 49 | # 50 | # see also: self.find_root_aliases() 51 | self._root_aliases = {} 52 | 53 | # Allow settings to override default attributes 54 | # We check for existing WHITENOISE_{} stuff to be compatible, but then 55 | # add a few RedNoise specific ones in order to not be too confusing. 56 | self.check_and_set_settings('WHITENOISE_{}', self.config_attrs) 57 | self.check_and_set_settings('REDNOISE_{}', self.rednoise_config_attrs) 58 | 59 | # If DEBUG=True in settings, then we'll just default Rednoise to debug. 60 | try: 61 | setattr(self, 'debug', getattr(settings, 'DEBUG')) 62 | except AttributeError: 63 | pass 64 | 65 | if self.should_serve_media: 66 | self.media_root, self.media_prefix = self.get_structure('MEDIA') 67 | 68 | # Grab the various roots we care about. 69 | if self.should_serve_static: 70 | self.static_root, self.static_prefix = self.get_structure('STATIC') 71 | 72 | try: 73 | setattr(self, 'staticfiles_dirs', getattr( 74 | settings, 'STATICFILES_DIRS' 75 | )) 76 | except AttributeError: 77 | pass 78 | 79 | self.make_root_aliases() 80 | 81 | def make_root_aliases(self): 82 | """For any static files that should be loading from the "root" - 83 | (e.g, robots.txt, favicon.ico, humans.txt, sitemap.xml, etc) we want 84 | to catch them and redirect to the actual static file. This basically 85 | just computes the necessary redirect paths for use in __call__. 86 | 87 | Users can specify overrides via REDNOISE_ROOT_ALIASES in settings.py. 88 | 89 | We do this because the favicon gets requested a lot, and in really 90 | weird circumstances sometimes. I don't like making Django deal with it 91 | just to serve a 301, so we just do it here. If the user lacks a file, 92 | it's just a proper 404 ultimately. 93 | """ 94 | try: 95 | static_url = getattr(settings, 'STATIC_URL') 96 | except: 97 | raise ImproperlyConfigured('STATIC_URL is not configured.') 98 | 99 | for alias in self.root_aliases: 100 | self._root_aliases[alias] = static_url + alias.replace('/', '') 101 | 102 | def __call__(self, environ, start_response): 103 | """Checks to see if a request is inside our designated media or static 104 | configurations. 105 | """ 106 | path = environ[PI] 107 | if path in self.root_aliases: 108 | start_response(A301, [('Location', self._root_aliases[path])]) 109 | return [] 110 | 111 | if self.should_serve_static and self.is_static(path): 112 | asset = self.load_static_file(path) 113 | if asset is not None: 114 | return self.serve(asset, environ, start_response) 115 | else: 116 | start_response(A404, [('Content-Type', 'text/plain')]) 117 | return [b'Not Found'] 118 | 119 | if self.should_serve_media and self.is_media(path): 120 | asset = self.load_media_file(path) 121 | if asset is not None: 122 | return self.serve(asset, environ, start_response) 123 | else: 124 | start_response(A404, [('Content-Type', 'text/plain')]) 125 | return [b'Not Found'] 126 | 127 | return self.application(environ, start_response) 128 | 129 | def file_not_modified(self, static_file, environ): 130 | """We just hook in here to always return false (i.e, it was modified) 131 | in DEBUG scenarios. This is optimal for development/reloading 132 | scenarios. 133 | 134 | In a production scenario, you want the original Whitenoise setup, so 135 | super(). 136 | """ 137 | if self.debug: 138 | return False 139 | return super(DjangoRedNoise, self).file_not_modified( 140 | static_file, 141 | environ 142 | ) 143 | 144 | def add_cache_headers(self, static_file, url): 145 | """Again, we hook in here to blank on adding cache headers in DEBUG 146 | scenarios. This is optimal for development/reloading 147 | scenarios. 148 | 149 | In a production scenario, you want the original Whitenoise setup, so 150 | super(). 151 | """ 152 | if self.debug: 153 | return 154 | super(DjangoRedNoise, self).add_cache_headers(static_file, url) 155 | 156 | def check_and_set_settings(self, settings_key, attributes): 157 | """Checks settings to see if we should override something. 158 | """ 159 | for attr in attributes: 160 | key = settings_key.format(attr.upper()) 161 | try: 162 | setattr(self, attr, getattr(settings, key)) 163 | except AttributeError: 164 | pass 165 | 166 | def get_structure(self, key): 167 | """This code is almost verbatim from the Whitenoise project Django 168 | integration. Little reason to change it, short of string substitution. 169 | """ 170 | url = getattr(settings, '%s_URL' % key, None) 171 | root = getattr(settings, '%s_ROOT' % key, None) 172 | if not url or not root: 173 | raise ImproperlyConfigured('%s_URL and %s_ROOT \ 174 | must be configured to use RedNoise' % (key, key)) 175 | prefix = urlparse.urlparse(url).path 176 | prefix = '/{}/'.format(prefix.strip('/')) 177 | return root, prefix 178 | 179 | def is_static(self, path): 180 | """Checks to see if a given path is trying to be all up in 181 | our static director(y||ies). 182 | """ 183 | return path[:len(self.static_prefix)] == self.static_prefix 184 | 185 | def add_static_file(self, path): 186 | """Custom, ish. Adopts the same approach as Whitenoise, but instead 187 | handles creating of a File object per each valid static/media request. 188 | This is then cached for lookup later if need-be. 189 | 190 | See also: self.add_media_file() 191 | """ 192 | file_path = self.find_static_file(path) 193 | 194 | if file_path: 195 | files = {} 196 | files[path] = self.get_static_file(file_path, path) 197 | 198 | if not self.debug: 199 | self.find_gzipped_alternatives(files) 200 | 201 | self.static_files.update(files) 202 | 203 | def find_static_file(self, path): 204 | """Finds a static file; if DEBUG mode is set for Django, it will 205 | mimic Django's default static files behavior and serve 206 | (and not cache) the file. If DEBUG mode is False, it will essentially 207 | mimic the default WhiteNoise behavior. 208 | """ 209 | file_path = None 210 | if self.debug: 211 | file_path = finders.find(path.replace(self.static_prefix, '', 1)) 212 | 213 | # The immediate assumption would be to just only do this in non-DEBUG 214 | # scenarios, but this here allows us to fall through to ROOT in DEBUG. 215 | if file_path is None: 216 | file_path = ('%s/%s' % ( 217 | self.static_root, path.replace(self.static_prefix, '', 1) 218 | )).replace('\\', '/') 219 | 220 | if not isfile(file_path): 221 | return None 222 | 223 | return file_path 224 | 225 | def load_static_file(self, path): 226 | """Retrieves a static file, optimizing along the way. 227 | Very possible it can return None. TODO: perhaps optimize that 228 | use case somehow. 229 | """ 230 | asset = self.static_files.get(path) 231 | if asset is None or self.debug: 232 | self.add_static_file(path) 233 | asset = self.static_files.get(path) 234 | 235 | return asset 236 | 237 | def is_media(self, path): 238 | """Checks to see if a given path is trying to be all up in our 239 | media director(y||ies). 240 | """ 241 | return path[:len(self.media_prefix)] == self.media_prefix 242 | 243 | def add_media_file(self, path): 244 | """Custom, ish. Adopts the same approach as Whitenoise, but instead 245 | handles creating of a File object per each valid static/media request. 246 | This is then cached for lookup later if need-be. 247 | 248 | Media and static assets have differing properties by their very 249 | nature, so we have separate methods. 250 | """ 251 | file_path = ('%s/%s' % ( 252 | self.media_root, path.replace(self.media_prefix, '', 1) 253 | )).replace('\\', '/') 254 | if isfile(file_path): 255 | files = {} 256 | files[path] = self.get_static_file(file_path, path) 257 | self.media_files.update(files) 258 | 259 | def load_media_file(self, path): 260 | """Retrieves a media file, optimizing along the way. 261 | """ 262 | asset = self.media_files.get(path) 263 | if asset is None or self.debug: 264 | self.add_media_file(path) 265 | asset = self.media_files.get(path) 266 | 267 | return asset 268 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | description-file = README.rst 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import codecs 3 | from setuptools import setup, find_packages 4 | 5 | 6 | PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__)) 7 | 8 | def read(*path): 9 | full_path = os.path.join(PROJECT_ROOT, *path) 10 | with codecs.open(full_path, 'r', encoding='utf-8') as f: 11 | return f.read() 12 | 13 | setup( 14 | name='django-rednoise', 15 | version='1.0.5', 16 | author='Ryan McGrath', 17 | author_email='ryan@venodesigns.net', 18 | url='https://github.com/ryanmcgrath/django-rednoise/', 19 | packages=find_packages(exclude=['tests*']), 20 | install_requires=['django', 'whitenoise'], 21 | license='MIT', 22 | description="Opinionated Django-specific addon for Whitenoise.", 23 | long_description=read('README.rst'), 24 | keywords=['django', 'static', 'wsgi'], 25 | classifiers=[ 26 | 'Development Status :: 5 - Production/Stable', 27 | 'Framework :: Django', 28 | 'Intended Audience :: Developers', 29 | 'License :: OSI Approved :: MIT License', 30 | 'Operating System :: OS Independent', 31 | 'Programming Language :: Python :: 2.7', 32 | 'Programming Language :: Python :: 3.3', 33 | 'Programming Language :: Python :: 3.4', 34 | ], 35 | ) 36 | --------------------------------------------------------------------------------