├── .gitchangelog.rc ├── .gitignore ├── .travis.yml ├── CHANGELOG ├── LICENSE ├── MANIFEST.in ├── README.md ├── django_cloudflare_push ├── __init__.py └── middleware.py ├── setup.cfg ├── setup.py └── tox.ini /.gitchangelog.rc: -------------------------------------------------------------------------------- 1 | ## 2 | ## Format 3 | ## 4 | ## ACTION: [AUDIENCE:] COMMIT_MSG [!TAG ...] 5 | ## 6 | ## Description 7 | ## 8 | ## ACTION is one of 'chg', 'fix', 'new' 9 | ## 10 | ## Is WHAT the change is about. 11 | ## 12 | ## 'chg' is for refactor, small improvement, cosmetic changes... 13 | ## 'fix' is for bug fixes 14 | ## 'new' is for new features, big improvement 15 | ## 16 | ## AUDIENCE is optional and one of 'dev', 'usr', 'pkg', 'test', 'doc' 17 | ## 18 | ## Is WHO is concerned by the change. 19 | ## 20 | ## 'dev' is for developpers (API changes, refactors...) 21 | ## 'usr' is for final users (UI changes) 22 | ## 'pkg' is for packagers (packaging changes) 23 | ## 'test' is for testers (test only related changes) 24 | ## 'doc' is for doc guys (doc only changes) 25 | ## 26 | ## COMMIT_MSG is ... well ... the commit message itself. 27 | ## 28 | ## TAGs are additionnal adjective as 'refactor' 'minor' 'cosmetic' 29 | ## 30 | ## They are preceded with a '!' or a '@' (prefer the former, as the 31 | ## latter is wrongly interpreted in github.) Commonly used tags are: 32 | ## 33 | ## 'refactor' is obviously for refactoring code only 34 | ## 'minor' is for a very meaningless change (a typo, adding a comment) 35 | ## 'cosmetic' is for cosmetic driven change (re-indentation, 80-col...) 36 | ## 'wip' is for partial functionality but complete subfunctionality. 37 | ## 38 | ## Example: 39 | ## 40 | ## new: usr: support of bazaar implemented 41 | ## chg: re-indentend some lines !cosmetic 42 | ## new: dev: updated code to be compatible with last version of killer lib. 43 | ## fix: pkg: updated year of licence coverage. 44 | ## new: test: added a bunch of test around user usability of feature X. 45 | ## fix: typo in spelling my name in comment. !minor 46 | ## 47 | ## Please note that multi-line commit message are supported, and only the 48 | ## first line will be considered as the "summary" of the commit message. So 49 | ## tags, and other rules only applies to the summary. The body of the commit 50 | ## message will be displayed in the changelog without reformatting. 51 | 52 | 53 | ## 54 | ## ``ignore_regexps`` is a line of regexps 55 | ## 56 | ## Any commit having its full commit message matching any regexp listed here 57 | ## will be ignored and won't be reported in the changelog. 58 | ## 59 | ignore_regexps = [ 60 | r'@minor', r'!minor', 61 | r'@cosmetic', r'!cosmetic', 62 | r'@refactor', r'!refactor', 63 | r'@wip', r'!wip', 64 | r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[p|P]kg:', 65 | r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[d|D]ev:', 66 | r'^(.{3,3}\s*:)?\s*[fF]irst commit.?\s*$', 67 | ] 68 | 69 | 70 | ## ``section_regexps`` is a list of 2-tuples associating a string label and a 71 | ## list of regexp 72 | ## 73 | ## Commit messages will be classified in sections thanks to this. Section 74 | ## titles are the label, and a commit is classified under this section if any 75 | ## of the regexps associated is matching. 76 | ## 77 | section_regexps = [ 78 | ('Features', [ 79 | r'^[Ff]eat(ure|)\s*:\s*([^\n]*)$', 80 | ]), 81 | ('Fixes', [ 82 | r'^[Ff]ix\s*:\s*([^\n]*)$', 83 | ]), 84 | ('Documentation', [ 85 | r'^[Dd]oc\s*:\s*([^\n]*)$', 86 | ]), 87 | ] 88 | 89 | 90 | ## ``body_process`` is a callable 91 | ## 92 | ## This callable will be given the original body and result will 93 | ## be used in the changelog. 94 | ## 95 | ## Available constructs are: 96 | ## 97 | ## - any python callable that take one txt argument and return txt argument. 98 | ## 99 | ## - ReSub(pattern, replacement): will apply regexp substitution. 100 | ## 101 | ## - Indent(chars=" "): will indent the text with the prefix 102 | ## Please remember that template engines gets also to modify the text and 103 | ## will usually indent themselves the text if needed. 104 | ## 105 | ## - Wrap(regexp=r"\n\n"): re-wrap text in separate paragraph to fill 80-Columns 106 | ## 107 | ## - noop: do nothing 108 | ## 109 | ## - ucfirst: ensure the first letter is uppercase. 110 | ## (usually used in the ``subject_process`` pipeline) 111 | ## 112 | ## - final_dot: ensure text finishes with a dot 113 | ## (usually used in the ``subject_process`` pipeline) 114 | ## 115 | ## - strip: remove any spaces before or after the content of the string 116 | ## 117 | ## Additionally, you can `pipe` the provided filters, for instance: 118 | #body_process = Wrap(regexp=r'\n(?=\w+\s*:)') | Indent(chars=" ") 119 | #body_process = Wrap(regexp=r'\n(?=\w+\s*:)') 120 | #body_process = noop 121 | body_process = ReSub(r'((^|\n)[A-Z]\w+(-\w+)*: .*(\n\s+.*)*)+$', r'') | strip 122 | 123 | 124 | ## ``subject_process`` is a callable 125 | ## 126 | ## This callable will be given the original subject and result will 127 | ## be used in the changelog. 128 | ## 129 | ## Available constructs are those listed in ``body_process`` doc. 130 | subject_process = (strip | 131 | ReSub(r'^([fF]ix|[fF]eat|[fF]eature|[dD]oc)\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n@]*)(@[a-z]+\s+)*$', r'\4') | 132 | ucfirst | final_dot) 133 | 134 | ## ``tag_filter_regexp`` is a regexp 135 | ## 136 | ## Tags that will be used for the changelog must match this regexp. 137 | ## 138 | tag_filter_regexp = r'^v?[0-9]+\.[0-9]+(\.[0-9]+)?$' 139 | 140 | 141 | ## ``unreleased_version_label`` is a string 142 | ## 143 | ## This label will be used as the changelog Title of the last set of changes 144 | ## between last valid tag and HEAD if any. 145 | unreleased_version_label = "%%version%% (unreleased)" 146 | 147 | 148 | ## ``output_engine`` is a callable 149 | ## 150 | ## This will change the output format of the generated changelog file 151 | ## 152 | ## Available choices are: 153 | ## 154 | ## - rest_py 155 | ## 156 | ## Legacy pure python engine, outputs ReSTructured text. 157 | ## This is the default. 158 | ## 159 | ## - mustache() 160 | ## 161 | ## Template name could be any of the available templates in 162 | ## ``templates/mustache/*.tpl``. 163 | ## Requires python package ``pystache``. 164 | ## Examples: 165 | ## - mustache("markdown") 166 | ## - mustache("restructuredtext") 167 | ## 168 | ## - makotemplate() 169 | ## 170 | ## Template name could be any of the available templates in 171 | ## ``templates/mako/*.tpl``. 172 | ## Requires python package ``mako``. 173 | ## Examples: 174 | ## - makotemplate("restructuredtext") 175 | ## 176 | output_engine = rest_py 177 | #output_engine = mustache("restructuredtext") 178 | #output_engine = mustache("markdown") 179 | #output_engine = makotemplate("restructuredtext") 180 | 181 | 182 | ## ``include_merge`` is a boolean 183 | ## 184 | ## This option tells git-log whether to include merge commits in the log. 185 | ## The default is to include them. 186 | include_merge = True 187 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | *.log 4 | *.pot 5 | *.pyc 6 | .coverage 7 | .tox 8 | *.egg 9 | *.egg-info 10 | local_settings.py 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 3.6 4 | cache: pip 5 | install: travis_retry pip install "virtualenv<14.0.0" tox codecov 6 | script: tox -e $TOX_ENV 7 | env: 8 | - TOX_ENV=django111-py36 9 | - TOX_ENV=django111-py35 10 | - TOX_ENV=django111-py34 11 | - TOX_ENV=django111-py27 12 | - TOX_ENV=django110-py35 13 | - TOX_ENV=django110-py34 14 | - TOX_ENV=django110-py27 15 | - TOX_ENV=django19-py35 16 | - TOX_ENV=django19-py34 17 | - TOX_ENV=django19-py27 18 | - TOX_ENV=django18-py35 19 | - TOX_ENV=django18-py34 20 | - TOX_ENV=django18-py27 21 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skorokithakis/django-cloudflare-push/eea2e59f83fcd030d0dbdb423a91b4013ee530f6/CHANGELOG -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Stavros Korokithakis 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | 11 | Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | Neither the name of Stavros Korokithakis nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include django_cloudflare_push *.py *.html *.txt 2 | include README.md LICENSE 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | django-cloudflare-push 2 | ====================== 3 | 4 | About 5 | ----- 6 | 7 | django-cloudflare-push is a small piece of middleware that looks at the list of 8 | static files in each page that is requested (you need to be using some sort of 9 | static files processor, like Django's built-in one), and [adds a Link 10 | header](https://www.cloudflare.com/website-optimization/http2/serverpush/) that 11 | CloudFlare uses to push the static files to the browser before the latter 12 | requests them, using HTTP/2 Push. 13 | 14 | Somewhat counter-intuitively, django-cloudflare-push is compatible with *any* 15 | provider that supports HTTP/2 Push using Link headers, which is pretty much 16 | most of them. For example, the Caddy webserver supports this with the `push` 17 | directive, and this library will work just fine with that. 18 | 19 | [![PyPI version](https://img.shields.io/pypi/v/django-cloudflare-push.svg)](https://pypi.python.org/pypi/django-cloudflare-push) 20 | 21 | 22 | 23 | Installing django-cloudflare-push 24 | --------------------------------- 25 | 26 | * Install django-cloudflare-push using pip: `pip install django-cloudflare-push` 27 | 28 | * Add the middleware to your MIDDLEWARE setting: 29 | 30 | ```python 31 | MIDDLEWARE = ( 32 | 'django_cloudflare_push.middleware.push_middleware', 33 | ... 34 | ) 35 | ``` 36 | 37 | Done! Your static media will be pushed. You can test the middleware by looking 38 | for the `Link` header. 39 | 40 | Settings 41 | -------- 42 | 43 | ```python 44 | CLOUDFLARE_PUSH_FILTER = lambda file: True 45 | ``` 46 | 47 | Allows you to customize what files will be sent to the client to be preloaded. 48 | This setting should be set to a callable, which accepts a single parameter 49 | (the name of the file to preload). By default, `django-cloudflare-push` pushes 50 | all static files. 51 | 52 | For instance, to push _only_ static CSS and JavaScript files: 53 | 54 | ```python 55 | CLOUDFLARE_PUSH_FILTER = lambda x: x.endswith('.css') or x.endswith('.js') 56 | ``` 57 | 58 | Webserver configuration 59 | ----------------------- 60 | 61 | Here's how to configure various webservers to work well with 62 | `django-cloudflare-push`: 63 | 64 | ### nginx 65 | 66 | If you're running nginx v1.13.9 or later, you can just include the 67 | `http2_push_preload on` directive in your configuration: 68 | 69 | ``` 70 | server { 71 | ... 72 | http2_push_preload on; 73 | ... 74 | } 75 | ``` 76 | 77 | ### Caddy 78 | 79 | With Caddy, you can use the [`push` directive](https://caddyserver.com/docs/push): 80 | 81 | ``` 82 | push 83 | ``` 84 | 85 | ...I know. 86 | 87 | License 88 | ------- 89 | 90 | This software is distributed under the BSD license. 91 | -------------------------------------------------------------------------------- /django_cloudflare_push/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.2.2' 2 | -------------------------------------------------------------------------------- /django_cloudflare_push/middleware.py: -------------------------------------------------------------------------------- 1 | """Parse a page and add a Link header to the request, which CloudFlare can use to push static media to an HTTP/2 client.""" 2 | 3 | from django.conf import settings 4 | from django.contrib.staticfiles import storage 5 | try: 6 | from django.contrib.staticfiles.templatetags import staticfiles 7 | except ImportError: 8 | # django.contrib.staticfiles.templatetags.staticfiles removed in django 3.0 9 | # https://github.com/django/django/blob/a6b3938afc0204093b5356ade2be30b461a698c5/docs/releases/3.0.txt#L661 10 | from django.contrib.staticfiles import storage as staticfiles 11 | from django.core.files.storage import get_storage_class 12 | from django.utils.functional import LazyObject 13 | 14 | 15 | EXTENSION_AS = { 16 | 'js': 'script', 17 | 'css': 'style', 18 | 'png': 'image', 19 | 'jpg': 'image', 20 | 'jpeg': 'image', 21 | 'svg': 'image', 22 | 'gif': 'image', 23 | 'webp': 'image', 24 | 'ttf': 'font', 25 | 'woff': 'font', 26 | 'woff2': 'font' 27 | } 28 | FILE_FILTER = getattr(settings, 'CLOUDFLARE_PUSH_FILTER', lambda x: True) 29 | 30 | 31 | class FileCollector(object): 32 | def __init__(self): 33 | self.collection = [] 34 | 35 | def collect(self, path): 36 | if not path.endswith('/') and FILE_FILTER(path.lower()): 37 | self.collection.append(path) 38 | 39 | 40 | def storage_factory(collector): 41 | class DebugConfiguredStorage(LazyObject): 42 | def _setup(self): 43 | configured_storage_cls = get_storage_class(settings.STATICFILES_STORAGE) 44 | 45 | class DebugStaticFilesStorage(configured_storage_cls): 46 | 47 | def __init__(self, collector, *args, **kwargs): 48 | super(DebugStaticFilesStorage, self).__init__(*args, **kwargs) 49 | self.collector = collector 50 | 51 | def url(self, path): 52 | self.collector.collect(path) 53 | return super(DebugStaticFilesStorage, self).url(path) 54 | 55 | self._wrapped = DebugStaticFilesStorage(collector) 56 | return DebugConfiguredStorage 57 | 58 | 59 | def sort_urls(urls): 60 | """ 61 | Order URLs by extension. 62 | This function accepts a list of URLs and orders them by their extension. 63 | CSS files are sorted to the start of the list, then JS, then everything 64 | else. 65 | """ 66 | order = {"css": 0, "js": 1} 67 | urls.sort(key=lambda x: order.get(x.rsplit(".")[-1].lower(), 2)) 68 | return urls 69 | 70 | 71 | def create_header_content(urls): 72 | """ 73 | Creates the content for the Link header. 74 | """ 75 | links = [] 76 | for url in urls[:10]: 77 | ext = url.rsplit(".")[-1].lower() 78 | if ext in EXTENSION_AS: 79 | link = "<%s>; rel=preload; as=%s" % (url, EXTENSION_AS[ext]) 80 | else: 81 | link = "<%s>; rel=preload" % (url,) 82 | links.append(link) 83 | return ", ".join(links) 84 | 85 | 86 | def push_middleware(get_response): 87 | def middleware(request): 88 | collector = FileCollector() 89 | storage.staticfiles_storage = staticfiles.staticfiles_storage = storage_factory(collector)() 90 | response = get_response(request) 91 | collection_copy = list(collector.collection) # For compatibility with 2.7. 92 | urls = list(set(storage.staticfiles_storage.url(f) for f in collection_copy)) 93 | urls = sort_urls(urls) 94 | response["Link"] = create_header_content(urls) 95 | return response 96 | return middleware 97 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | 4 | [semantic_release] 5 | version_variable = django_cloudflare_push/__init__.py:__version__ 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | from django_cloudflare_push import __version__ 5 | assert sys.version >= '2.7', "Requires Python v2.7 or above." 6 | from distutils.core import setup 7 | from setuptools import find_packages 8 | 9 | setup( 10 | name="django-cloudflare-push", 11 | version=__version__, 12 | author="Stavros Korokithakis", 13 | author_email="hi@stavros.io", 14 | url="https://github.com/skorokithakis/django-cloudflare-push", 15 | description="""An piece of middleware that tells CloudFlare to HTTP/2 Push static files in a page to the client.""", 16 | long_description="A piece of middleware that lists all the static media in a Django page and adds a header that instructs" 17 | " CloudFlare to use HTTP/2's Push functionality to send the media to the client before the latter requests them.", 18 | license="BSD", 19 | keywords="django, cloudflare, http2, push", 20 | zip_safe=False, 21 | include_package_data=True, 22 | packages=find_packages(), 23 | ) 24 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist={py27,py34,py35,py36}-django{18,19,110} 3 | 4 | [testenv] 5 | basepython= 6 | py27: python2.7 7 | py34: python3.4 8 | py35: python3.5 9 | py36: python3.6 10 | deps= 11 | django111: django==1.11 12 | commands=python setup.py test 13 | --------------------------------------------------------------------------------