203 lines
7.0 KiB
Python
203 lines
7.0 KiB
Python
|
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 .base import WhiteNoise
|
||
|
from .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
|