203 lines
6.5 KiB
Python
203 lines
6.5 KiB
Python
|
|
import logging
|
||
|
|
import re
|
||
|
|
from collections import defaultdict
|
||
|
|
from urllib.parse import urlsplit, urlunsplit
|
||
|
|
|
||
|
|
from django.conf import settings
|
||
|
|
from django.core.exceptions import ImproperlyConfigured
|
||
|
|
from django.utils.module_loading import import_string
|
||
|
|
|
||
|
|
from wagtail.coreutils import get_content_languages
|
||
|
|
|
||
|
|
logger = logging.getLogger("wagtail.frontendcache")
|
||
|
|
|
||
|
|
|
||
|
|
class InvalidFrontendCacheBackendError(ImproperlyConfigured):
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
def get_backends(backend_settings=None, backends=None):
|
||
|
|
# Get backend settings from WAGTAILFRONTENDCACHE setting
|
||
|
|
if backend_settings is None:
|
||
|
|
backend_settings = getattr(settings, "WAGTAILFRONTENDCACHE", None)
|
||
|
|
|
||
|
|
# Fallback to using WAGTAILFRONTENDCACHE_LOCATION setting (backwards compatibility)
|
||
|
|
if backend_settings is None:
|
||
|
|
cache_location = getattr(settings, "WAGTAILFRONTENDCACHE_LOCATION", None)
|
||
|
|
|
||
|
|
if cache_location is not None:
|
||
|
|
backend_settings = {
|
||
|
|
"default": {
|
||
|
|
"BACKEND": "wagtail.contrib.frontend_cache.backends.HTTPBackend",
|
||
|
|
"LOCATION": cache_location,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
# No settings found, return empty list
|
||
|
|
if backend_settings is None:
|
||
|
|
return {}
|
||
|
|
|
||
|
|
backend_objects = {}
|
||
|
|
|
||
|
|
for backend_name, _backend_config in backend_settings.items():
|
||
|
|
if backends is not None and backend_name not in backends:
|
||
|
|
continue
|
||
|
|
|
||
|
|
backend_config = _backend_config.copy()
|
||
|
|
backend = backend_config.pop("BACKEND")
|
||
|
|
|
||
|
|
# Try to import the backend
|
||
|
|
try:
|
||
|
|
backend_cls = import_string(backend)
|
||
|
|
except ImportError as e:
|
||
|
|
raise InvalidFrontendCacheBackendError(
|
||
|
|
f"Could not find backend '{backend}': {e}"
|
||
|
|
)
|
||
|
|
|
||
|
|
backend_objects[backend_name] = backend_cls(backend_config)
|
||
|
|
|
||
|
|
return backend_objects
|
||
|
|
|
||
|
|
|
||
|
|
def purge_url_from_cache(url, backend_settings=None, backends=None):
|
||
|
|
purge_urls_from_cache([url], backend_settings=backend_settings, backends=backends)
|
||
|
|
|
||
|
|
|
||
|
|
def purge_urls_from_cache(urls, backend_settings=None, backends=None):
|
||
|
|
# Convert each url to urls one for each managed language (WAGTAILFRONTENDCACHE_LANGUAGES setting).
|
||
|
|
# The managed languages are common to all the defined backends.
|
||
|
|
# This depends on settings.USE_I18N
|
||
|
|
# If WAGTAIL_I18N_ENABLED is True, this defaults to WAGTAIL_CONTENT_LANGUAGES
|
||
|
|
wagtail_i18n_enabled = getattr(settings, "WAGTAIL_I18N_ENABLED", False)
|
||
|
|
content_languages = get_content_languages() if wagtail_i18n_enabled else {}
|
||
|
|
languages = getattr(
|
||
|
|
settings, "WAGTAILFRONTENDCACHE_LANGUAGES", list(content_languages.keys())
|
||
|
|
)
|
||
|
|
if settings.USE_I18N and languages:
|
||
|
|
langs_regex = "^/(%s)/" % "|".join(languages)
|
||
|
|
new_urls = []
|
||
|
|
|
||
|
|
# Purge the given url for each managed language
|
||
|
|
for isocode in languages:
|
||
|
|
for url in urls:
|
||
|
|
up = urlsplit(url)
|
||
|
|
new_url = urlunsplit(
|
||
|
|
(
|
||
|
|
up.scheme,
|
||
|
|
up.netloc,
|
||
|
|
re.sub(langs_regex, "/%s/" % isocode, up.path),
|
||
|
|
up.query,
|
||
|
|
up.fragment,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
|
||
|
|
# Check for best performance. True if re.sub found no match
|
||
|
|
# It happens when i18n_patterns was not used in urls.py to serve content for different languages from different URLs
|
||
|
|
if new_url in new_urls:
|
||
|
|
continue
|
||
|
|
|
||
|
|
new_urls.append(new_url)
|
||
|
|
|
||
|
|
urls = new_urls
|
||
|
|
|
||
|
|
urls_by_hostname = defaultdict(list)
|
||
|
|
|
||
|
|
for url in urls:
|
||
|
|
urls_by_hostname[urlsplit(url).netloc].append(url)
|
||
|
|
|
||
|
|
backends = get_backends(backend_settings, backends)
|
||
|
|
|
||
|
|
for hostname, urls in urls_by_hostname.items():
|
||
|
|
backends_for_hostname = {
|
||
|
|
backend_name: backend
|
||
|
|
for backend_name, backend in backends.items()
|
||
|
|
if backend.invalidates_hostname(hostname)
|
||
|
|
}
|
||
|
|
|
||
|
|
if not backends_for_hostname:
|
||
|
|
logger.info("Unable to find purge backend for %s", hostname)
|
||
|
|
continue
|
||
|
|
|
||
|
|
for backend_name, backend in backends_for_hostname.items():
|
||
|
|
for url in urls:
|
||
|
|
logger.info("[%s] Purging URL: %s", backend_name, url)
|
||
|
|
|
||
|
|
backend.purge_batch(urls)
|
||
|
|
|
||
|
|
|
||
|
|
def _get_page_cached_urls(page):
|
||
|
|
page_url = page.full_url
|
||
|
|
if page_url is None: # nothing to be done if the page has no routable URL
|
||
|
|
return []
|
||
|
|
|
||
|
|
return [page_url + path.lstrip("/") for path in page.specific.get_cached_paths()]
|
||
|
|
|
||
|
|
|
||
|
|
def purge_page_from_cache(page, backend_settings=None, backends=None):
|
||
|
|
purge_pages_from_cache([page], backend_settings=backend_settings, backends=backends)
|
||
|
|
|
||
|
|
|
||
|
|
def purge_pages_from_cache(pages, backend_settings=None, backends=None):
|
||
|
|
urls = []
|
||
|
|
for page in pages:
|
||
|
|
urls.extend(_get_page_cached_urls(page))
|
||
|
|
|
||
|
|
if urls:
|
||
|
|
purge_urls_from_cache(urls, backend_settings, backends)
|
||
|
|
|
||
|
|
|
||
|
|
class PurgeBatch:
|
||
|
|
"""Represents a list of URLs to be purged in a single request"""
|
||
|
|
|
||
|
|
def __init__(self, urls=None):
|
||
|
|
self.urls = []
|
||
|
|
|
||
|
|
if urls is not None:
|
||
|
|
self.add_urls(urls)
|
||
|
|
|
||
|
|
def add_url(self, url):
|
||
|
|
"""Adds a single URL"""
|
||
|
|
self.urls.append(url)
|
||
|
|
|
||
|
|
def add_urls(self, urls):
|
||
|
|
"""
|
||
|
|
Adds multiple URLs from an iterable
|
||
|
|
|
||
|
|
This is equivalent to running ``.add_url(url)`` on each URL
|
||
|
|
individually
|
||
|
|
"""
|
||
|
|
self.urls.extend(urls)
|
||
|
|
|
||
|
|
def add_page(self, page):
|
||
|
|
"""
|
||
|
|
Adds all URLs for the specified page
|
||
|
|
|
||
|
|
This combines the page's full URL with each path that is returned by
|
||
|
|
the page's `.get_cached_paths` method
|
||
|
|
"""
|
||
|
|
self.add_urls(_get_page_cached_urls(page))
|
||
|
|
|
||
|
|
def add_pages(self, pages):
|
||
|
|
"""
|
||
|
|
Adds multiple pages from a QuerySet or an iterable
|
||
|
|
|
||
|
|
This is equivalent to running ``.add_page(page)`` on each page
|
||
|
|
individually
|
||
|
|
"""
|
||
|
|
for page in pages:
|
||
|
|
self.add_page(page)
|
||
|
|
|
||
|
|
def purge(self, backend_settings=None, backends=None):
|
||
|
|
"""
|
||
|
|
Performs the purge of all the URLs in this batch
|
||
|
|
|
||
|
|
This method takes two optional keyword arguments: backend_settings and backends
|
||
|
|
|
||
|
|
- backend_settings can be used to override the WAGTAILFRONTENDCACHE setting for
|
||
|
|
just this call
|
||
|
|
|
||
|
|
- backends can be set to a list of backend names. When set, the invalidation request
|
||
|
|
will only be sent to these backends
|
||
|
|
"""
|
||
|
|
purge_urls_from_cache(self.urls, backend_settings, backends)
|