Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Django Middleware that Delays Traversal of Static Root until Requested #275

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
25 changes: 19 additions & 6 deletions whitenoise/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,18 +96,31 @@ def serve(static_file, environ, start_response):
return file_wrapper(response.file)
else:
return []

def normalize_root(self, root):
value = decode_if_byte_string(root, force_text=True)
value = os.path.abspath(value)
value = value.rstrip(os.path.sep) + os.path.sep
return value

def normalize_prefix(self, prefix):
value = decode_if_byte_string(prefix)
value = ensure_leading_trailing_slash(value)
return value

def insert_directory(self, root, prefix):
root = self.normalize_root(root)
prefix = self.normalize_prefix(prefix)
self.directories.insert(0, (root, prefix))

def add_files(self, root, prefix=None):
root = decode_if_byte_string(root, force_text=True)
root = os.path.abspath(root)
root = root.rstrip(os.path.sep) + os.path.sep
prefix = decode_if_byte_string(prefix)
prefix = ensure_leading_trailing_slash(prefix)
root = self.normalize_root(root)
prefix = self.normalize_prefix(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))
self.insert_directory(root, prefix)
else:
if os.path.isdir(root):
self.update_files_dictionary(root, prefix)
Expand Down
44 changes: 41 additions & 3 deletions whitenoise/middleware.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
import logging
import os
from itertools import chain
from posixpath import basename
from urllib.parse import urlparse

from django.conf import settings
from django.contrib.staticfiles.storage import staticfiles_storage
from django.contrib.staticfiles.storage import staticfiles_storage, ManifestStaticFilesStorage
from django.contrib.staticfiles import finders
from django.http import FileResponse
from django.urls import get_script_prefix
from django.utils.functional import cached_property

from .base import WhiteNoise
from .string_utils import decode_if_byte_string, ensure_leading_trailing_slash


__all__ = ["WhiteNoiseMiddleware"]
__all__ = [
"WhiteNoiseMiddleware",
"LazyWhiteNoiseMiddleware",
]


logger = logging.getLogger(__name__)


class WhiteNoiseFileResponse(FileResponse):
Expand Down Expand Up @@ -40,19 +49,26 @@ class WhiteNoiseMiddleware(WhiteNoise):
root = None
use_finders = False
static_prefix = None
add_static_root_files = True

def __init__(self, get_response=None, settings=settings):
self.get_response = get_response
self.configure_from_settings(settings)
# Pass None for `application`
super(WhiteNoiseMiddleware, self).__init__(None)
if self.static_root:
self.add_files(self.static_root, prefix=self.static_prefix)
if self.add_static_root_files:
self.add_files(self.static_root, prefix=self.static_prefix)
else:
self.insert_directory(self.static_root, self.static_prefix)
if self.root:
self.add_files(self.root)
if self.use_finders and not self.autorefresh:
self.add_files_from_finders()

def can_lazy_load_url(self, url):
return False

def __call__(self, request):
response = self.process_request(request)
if response is None:
Expand All @@ -64,6 +80,11 @@ def process_request(self, request):
static_file = self.find_file(request.path_info)
else:
static_file = self.files.get(request.path_info)
if static_file is None and self.can_lazy_load_url(request.path_info):
static_file = self.find_file(request.path_info)
if static_file is not None:
logger.info('Lazy loaded %s', request.path_info)
self.files[request.path_info] = static_file
if static_file is not None:
return self.serve(static_file, request)

Expand Down Expand Up @@ -168,3 +189,20 @@ def get_static_url(self, name):
return decode_if_byte_string(staticfiles_storage.url(name))
except ValueError:
return None


class LazyWhiteNoiseMiddleware(WhiteNoiseMiddleware):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would personally implement this feature directly into the original middleware class, and then have it toggleable by a settings.py:WHITENOISE_LAZY value.

I'd also like to point out I agree with your points on ManifestStaticFilesStorage. I would assume there isn't any directory traversal when using the manifest, but you'll need to confirm that.

add_static_root_files = False

@cached_property
def known_static_urls(self):
if isinstance(staticfiles_storage, ManifestStaticFilesStorage):
serve_unhashed = not getattr(settings, "WHITENOISE_KEEP_ONLY_HASHED_FILES", False)
return set(['{}{}'.format(self.static_prefix, n) for n in chain(
staticfiles_storage.hashed_files.values(),
staticfiles_storage.hashed_files.keys() if serve_unhashed else [],
)])

def can_lazy_load_url(self, url):
return url.startswith(self.static_prefix) and \
self.known_static_urls is None or url in self.known_static_urls