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 = ...