diff --git a/qlinks/admin.py b/qlinks/admin.py index 8603ac9..4b8825e 100644 --- a/qlinks/admin.py +++ b/qlinks/admin.py @@ -17,17 +17,17 @@ class LinkAdmin(admin.ModelAdmin): search_fields = ('short', 'long') @admin.display(ordering='short', description=_('short slug')) - def short_slug(self, obj): + def short_slug(self, obj: Link): return obj.short or '/' @admin.display(ordering='long', description=_('long URL')) - def long_url(self, obj): + def long_url(self, obj: Link): return format_html('{1}', obj.long, truncatechars(obj.long, 64)) @admin.display(description=_('link')) - def short_url(self, obj): + def short_url(self, obj: Link): if settings.QLINKS_CANONICAL: - return format_html('{2}', settings.QLINKS_CANONICAL, obj.short, gettext('Link')) + return format_html('{1}', obj.short_url, gettext('Link')) def save_model(self, request, obj: Link, form, change): obj.created_by = request.user @@ -35,6 +35,11 @@ class LinkAdmin(admin.ModelAdmin): obj.is_working = check_url(obj.long) obj.last_check = timezone.now() super().save_model(request, obj, form, change) + obj.purge_cdn() + + def delete_model(self, request, obj: Link): + super().delete_model(request, obj) + obj.purge_cdn() admin.site.register(Link, LinkAdmin) diff --git a/qlinks/cdn_cache/__init__.py b/qlinks/cdn_cache/__init__.py new file mode 100644 index 0000000..4b5681f --- /dev/null +++ b/qlinks/cdn_cache/__init__.py @@ -0,0 +1,12 @@ +from typing import Optional + +from django.conf import settings +from django.utils.module_loading import import_string + +from qlinks.cdn_cache.base import BaseCDNCache + +cdn_cache: Optional[BaseCDNCache] +if settings.QLINKS_CDN_CACHE: + cdn_cache = import_string(settings.QLINKS_CDN_CACHE)() +else: + cdn_cache = None diff --git a/qlinks/cdn_cache/base.py b/qlinks/cdn_cache/base.py new file mode 100644 index 0000000..6fe83f4 --- /dev/null +++ b/qlinks/cdn_cache/base.py @@ -0,0 +1,9 @@ +import abc + + +class BaseCDNCache(abc.ABC): + @abc.abstractmethod + def __init__(self): ... + + @abc.abstractmethod + def purge(self, url: str) -> None: ... diff --git a/qlinks/cdn_cache/cloudflare_cache.py b/qlinks/cdn_cache/cloudflare_cache.py new file mode 100644 index 0000000..18c23c0 --- /dev/null +++ b/qlinks/cdn_cache/cloudflare_cache.py @@ -0,0 +1,18 @@ +from CloudFlare import CloudFlare +from django.conf import settings + +from qlinks.cdn_cache import BaseCDNCache + + +class CloudflareCDNCache(BaseCDNCache): + cf: CloudFlare + zone: str + + def __init__(self): + self.cf = CloudFlare(token=getattr(settings, 'QLINKS_CDN_CLOUDFLARE_API_TOKEN', None)) + self.zone = settings.QLINKS_CDN_CLOUDFLARE_ZONE_ID + + def purge(self, url: str) -> None: + self.cf.zones.purge_cache.post(self.zone, data={ + 'files': [url], + }) diff --git a/qlinks/models.py b/qlinks/models.py index 1e00989..f06a3f6 100644 --- a/qlinks/models.py +++ b/qlinks/models.py @@ -1,8 +1,12 @@ +from functools import cached_property + from django.conf import settings from django.db import models from django.utils import timezone from django.utils.translation import gettext_lazy as _ +from qlinks.cdn_cache import cdn_cache + class Link(models.Model): short = models.SlugField(max_length=64, verbose_name=_('short link slug'), unique=True, blank=True, @@ -18,3 +22,11 @@ class Link(models.Model): def __str__(self): return self.short or '/' + + @cached_property + def short_url(self): + return settings.QLINKS_CANONICAL + self.short + + def purge_cdn(self): + if cdn_cache: + cdn_cache.purge(self.short_url) diff --git a/qlinks/settings/base.py b/qlinks/settings/base.py index e7c7951..09dd77c 100644 --- a/qlinks/settings/base.py +++ b/qlinks/settings/base.py @@ -86,3 +86,4 @@ QLINKS_CANONICAL = None QLINKS_SITE_HEADER = 'QLinks Admin' QLINKS_SITE_TITLE = 'QLinks Admin' QLINKS_INDEX_TITLE = 'Welcome to the QLinks admin interface!' +QLINKS_CDN_CACHE = None diff --git a/qlinks/settings/template.py b/qlinks/settings/template.py index 6b7cd24..dd02744 100644 --- a/qlinks/settings/template.py +++ b/qlinks/settings/template.py @@ -27,3 +27,9 @@ QLINKS_ADMIN_HOST = r'admin' # Set to link prefix for short links, e.g. 'https://short.example.com/' QLINKS_CANONICAL = None + +# If you are using a CDN, you can optionally configure it to cache all the +# redirects, and then use a CDN cache backend to purge the cache. +# QLINKS_CDN_CACHE = 'qlinks.cdn_cache.cloudflare_cache.CloudflareCDNCache' +# QLINKS_CDN_CLOUDFLARE_API_TOKEN = ... +# QLINKS_CDN_CLOUDFLARE_API_ZONE_ID = ...