-
Notifications
You must be signed in to change notification settings - Fork 145
/
middleware.py
202 lines (177 loc) · 7.03 KB
/
middleware.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
from __future__ import annotations
import os
from posixpath import basename
from urllib.parse import urlparse
from django.conf import settings
from django.contrib.staticfiles import finders
from django.contrib.staticfiles.storage import staticfiles_storage
from django.http import FileResponse
from django.urls import get_script_prefix
from whitenoise.base import WhiteNoise
from whitenoise.string_utils import ensure_leading_trailing_slash
__all__ = ["WhiteNoiseMiddleware"]
class WhiteNoiseFileResponse(FileResponse):
"""
Wrap Django's FileResponse to prevent setting any default headers. For the
most part these just duplicate work already done by WhiteNoise but in some
cases (e.g. the content-disposition header introduced in Django 3.0) they
are actively harmful.
"""
def set_headers(self, *args, **kwargs):
pass
class WhiteNoiseMiddleware(WhiteNoise):
"""
Wrap WhiteNoise to allow it to function as Django middleware, rather
than WSGI middleware.
"""
def __init__(self, get_response=None, settings=settings):
self.get_response = get_response
try:
autorefresh: bool = settings.WHITENOISE_AUTOREFRESH
except AttributeError:
autorefresh = settings.DEBUG
try:
max_age = settings.WHITENOISE_MAX_AGE
except AttributeError:
if settings.DEBUG:
max_age = 0
else:
max_age = 60
try:
allow_all_origins = settings.WHITENOISE_ALLOW_ALL_ORIGINS
except AttributeError:
allow_all_origins = True
try:
charset = settings.WHITENOISE_CHARSET
except AttributeError:
charset = "utf-8"
try:
mimetypes = settings.WHITENOISE_MIMETYPES
except AttributeError:
mimetypes = None
try:
add_headers_function = settings.WHITENOISE_ADD_HEADERS_FUNCTION
except AttributeError:
add_headers_function = None
try:
index_file = settings.WHITENOISE_INDEX_FILE
except AttributeError:
index_file = None
try:
immutable_file_test = settings.WHITENOISE_IMMUTABLE_FILE_TEST
except AttributeError:
immutable_file_test = None
super().__init__(
application=None,
autorefresh=autorefresh,
max_age=max_age,
allow_all_origins=allow_all_origins,
charset=charset,
mimetypes=mimetypes,
add_headers_function=add_headers_function,
index_file=index_file,
immutable_file_test=immutable_file_test,
)
try:
self.use_finders = settings.WHITENOISE_USE_FINDERS
except AttributeError:
self.use_finders = settings.DEBUG
try:
self.static_prefix = settings.WHITENOISE_STATIC_PREFIX
except AttributeError:
self.static_prefix = urlparse(settings.STATIC_URL or "").path
script_prefix = get_script_prefix().rstrip("/")
if script_prefix:
if self.static_prefix.startswith(script_prefix):
self.static_prefix = self.static_prefix[len(script_prefix) :]
self.static_prefix = ensure_leading_trailing_slash(self.static_prefix)
self.static_root = settings.STATIC_ROOT
if self.static_root:
self.add_files(self.static_root, prefix=self.static_prefix)
try:
root = settings.WHITENOISE_ROOT
except AttributeError:
root = None
if root:
self.add_files(root)
if self.use_finders and not self.autorefresh:
self.add_files_from_finders()
def __call__(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)
return self.get_response(request)
@staticmethod
def serve(static_file, request):
response = static_file.get_response(request.method, request.META)
status = int(response.status)
http_response = WhiteNoiseFileResponse(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
def add_files_from_finders(self):
files = {}
for finder in finders.get_finders():
for path, storage in finder.list(None):
prefix = (getattr(storage, "prefix", None) or "").strip("/")
url = "".join(
(
self.static_prefix,
prefix,
"/" if prefix else "",
path.replace("\\", "/"),
)
)
# Use setdefault as only first matching file should be used
files.setdefault(url, storage.path(path))
stat_cache = {path: os.stat(path) for path in files.values()}
for url, path in files.items():
self.add_file_to_dictionary(url, path, stat_cache=stat_cache)
def candidate_paths_for_url(self, url):
if self.use_finders and url.startswith(self.static_prefix):
path = finders.find(url[len(self.static_prefix) :])
if path:
yield path
paths = super().candidate_paths_for_url(url)
for path in paths:
yield path
def immutable_file_test(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 staticfiles_storage.url(name)
except ValueError:
return None