Skip to content

Commit

Permalink
added middleware and fixed weird static file url mapping, also refact…
Browse files Browse the repository at this point in the history
…ored webp.py for readability

middleware added bc whitenoise only scans the static folder on start up so the newly generated files are not added to its hashmap, the modded middleware does a doublecheck to ensure the file actually exists if its not on the hashmap
  • Loading branch information
bimmui committed Sep 2, 2023
1 parent 0c39cdc commit 70883de
Show file tree
Hide file tree
Showing 8 changed files with 588 additions and 89 deletions.
16 changes: 13 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ add it to ``INSTALLED_APPS`` configuration
'django_webp',
'...',
)
edit your installation of Whitenoise to use our slightly modded version of it

.. code:: python
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django_webp.middleware.ModdedWhiteNoiseMiddleware',
'...',
]
add the django\_webp context processor

Expand Down Expand Up @@ -82,15 +92,15 @@ The following Django-level settings affect the behavior of the library
Ideally, this should temporarily be set to ``True`` whenever the ``WEBP_CACHE`` has been cleaned or if there has been substantial changes to your project's template files.
This defaults to ``False``.

- ``USING_WHITENOISE``

Set to ``True`` when whitenoise is used as middleware, defaults to ``False`` if not. Used to determine the directory to store the ``WEBP_CACHE``.



Possible problems
Possible Issues
-----------------

- ``django-webp`` uses ``Pillow`` to convert the images. If you’ve installed the ``libwebp-dev`` after already installed ``Pillow``, it’s necessary to uninstall and install it back because it needs to be compiled with it.
- This package was built specifically for production environments that use Whitenoise for serving static files so there currently isn't support for serving files via dedicated servers or through cloud services

Cleaning the cache
------------------
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"

[project]
name = "django-webp"
version = "2.1.0"
description = "Returns a webp image instead of jpg, gif or png to browsers"
version = "3.0.0"
description = "Serves a webp version of static images to browsers instead of jpg, gif or png"
readme = "README.rst"
requires-python = ">=3.5"
keywords = ["django", "webp", "python"]
Expand Down Expand Up @@ -34,6 +34,7 @@ dependencies = [
"django>=4.0.3",
"pillow>=9.0.1",
"sqlparse>=0.4.2",
"whitenoise>=6.5.0"
]

[project.optional-dependencies]
Expand Down
219 changes: 219 additions & 0 deletions src/django_webp/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
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 ModdedWhiteNoiseMiddleware(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)
if static_file is None:
if self.static_prefix in request.path_info:
# using path info and static root to check the directory of the recently made webp
raw_full_path = os.path.join(self.static_root, (request.path_info).lstrip("/"))
clean_full_path = raw_full_path.replace("\\", "/")
complete_full_path = clean_full_path.replace(self.static_prefix.rstrip("/"), "", 1)
if os.path.exists(complete_full_path):
self.add_files(self.static_root, prefix=self.static_prefix)
static_file = self.files.get(complete_full_path)
if static_file is None:
request.not_serving = request.path_info
return self.serve(static_file, request)
return self.get_response(request)

@staticmethod
def serve(static_file, request):
try:
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
except:
http_response = WhiteNoiseFileResponse((), status=500)
http_response['not serving'] = request.not_serving
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
Loading

0 comments on commit 70883de

Please sign in to comment.