django-cloudflare-images
A Django integration for Cloudflare Images, providing a storage backend, model fields, and a Direct Creator Upload widget.
It supports:
- Django 3
- Django 4
- Django 5
- Django 6
Installation
pip install django-cloudflare-images
Python 3.10 or later is required.
Setup
Add your Cloudflare Images credentials to settings.py:
CLOUDFLARE_IMAGES_ACCOUNT_ID = "XXX"
CLOUDFLARE_IMAGES_API_TOKEN = "YYY"
CLOUDFLARE_IMAGES_ACCOUNT_HASH = "ZZZ"
Then configure the storage backend.
For Django 4.2+:
STORAGES = {
"default": {
"BACKEND": "cloudflare_images.storage.CloudflareImagesStorage",
},
}
For older Django versions:
DEFAULT_FILE_STORAGE = "cloudflare_images.storage.CloudflareImagesStorage"
Optional settings
| Setting | Default | Description |
|---|---|---|
CLOUDFLARE_IMAGES_DOMAIN |
None |
Custom domain used to serve images. |
CLOUDFLARE_IMAGES_VARIANT |
"public" |
Default variant used to serve images. |
CLOUDFLARE_IMAGES_API_TIMEOUT |
60 |
Timeout for Cloudflare API requests, in seconds. |
CLOUDFLARE_IMAGES_UPLOAD_ENDPOINT |
"/ext/cloudflare_images/api" |
Endpoint used to retrieve Direct Creator Upload URLs. |
CLOUDFLARE_IMAGES_SIGNING_KEY |
None |
Signing key used to serve private images. |
CLOUDFLARE_IMAGES_SIGNED_URL_TTL |
86400 |
Lifetime of signed URLs, in seconds. |
Usage
Per-field variants
To use a specific variant for a field, replace ImageField with
CloudflareImagesField:
from django.db import models
from cloudflare_images.field import CloudflareImagesField
class MyModel(models.Model):
image = CloudflareImagesField(variant="custom")
You will need to generate a migration after swapping the fields. No SQL is
applied; you can verify this with sqlmigrate <app> <migration>.
Checking if an image exists
Use the storage's exists() method:
from django.core.files.storage import default_storage
if default_storage.exists("image_id"):
print("Image exists!")
else:
print("Image not found")
Note: This makes an HTTP API call to Cloudflare on every invocation. If you check many images, consider adding your own caching layer.
Private Images and Signed URLs
Cloudflare Images can serve private images that require a signed URL. Add your signing key from the Cloudflare dashboard (Images > Keys):
CLOUDFLARE_IMAGES_SIGNING_KEY = "..."
Server-side uploads
Use a storage configured with require_signed_urls=True and attach it to your
field:
from django.db import models
from cloudflare_images.field import CloudflareImagesField
from cloudflare_images.storage import CloudflareImagesStorage
private_storage = CloudflareImagesStorage(require_signed_urls=True)
class Document(models.Model):
image = CloudflareImagesField(storage=private_storage)
With this storage:
- uploads are created with
requireSignedURLs=true; document.image.urlreturns a signed URL;storage.open()andstorage.size()use signed URLs internally.
If the whole project uses private images, you can configure the default storage instead of attaching a storage to each field (Django 4.2+):
STORAGES = {
"default": {
"BACKEND": "cloudflare_images.storage.CloudflareImagesStorage",
"OPTIONS": {"require_signed_urls": True},
},
}
Explicit signed URLs
You can generate a signed URL explicitly, with a custom lifetime in seconds:
url = document.image.signed_url(expires_in=300)
# Or, without a field:
url = private_storage.signed_url("image_id", "public", expires_in=300)
If expires_in is omitted, CLOUDFLARE_IMAGES_SIGNED_URL_TTL is used (24 hours
by default).
Caveats
- Generate signed URLs when rendering; do not store them, as they expire.
- Rotating the signing key invalidates all existing signed URLs.
- Cloudflare's custom paths feature does not support private images.
- Images uploaded with a custom ID cannot be made private with signed URLs.
Direct Creator Upload
Cloudflare's Direct Creator Upload lets users upload images directly to Cloudflare without exposing your API token.
Setup
Add the app to INSTALLED_APPS:
INSTALLED_APPS = [
# ...
"cloudflare_images",
]
Add the endpoint to your URL configuration:
from cloudflare_images.views import WidgetAPI
# Add this inside your urlpatterns:
path("ext/cloudflare_images/api", WidgetAPI.as_view(), name="widget-api"),
Note: The provided endpoint has no security and should be treated as an example. It is strongly recommended to implement your own endpoint. See cloudflare_images/views.py for the base implementation.
To point the widget at a custom endpoint:
CLOUDFLARE_IMAGES_UPLOAD_ENDPOINT = "/my/endpoint/api"
Model
Replace your image field with CloudflareImageIDField:
from cloudflare_images.field import CloudflareImageIDField
# In your model:
image = CloudflareImageIDField(variant="custom")
Generate a migration after changing the field. This field is not backward
compatible with CloudflareImagesField, particularly for .url; use a helper
to generate URLs.
Template
Load the widget assets from your form:
{{ form.media.js }}
{{ form.media.css }}
Private uploads
To create private images, subclass the provided view and request private uploads:
from cloudflare_images.views import WidgetAPI
class PrivateWidgetAPI(WidgetAPI):
def get_upload_options(self, request):
return {"require_signed_urls": True}
Use this view in your URL configuration, and enable signed previews in the form:
from django import forms
from cloudflare_images.widget import CloudflareImagesWidget
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ["image"]
widgets = {"image": CloudflareImagesWidget(signed=True)}
After a private upload, the widget displays the image ID without a link until the form is saved and the page is rendered again.
Generating URLs
CloudflareImageIDField stores only the image ID, so use the service to build
the URL:
from cloudflare_images.service import CloudflareImagesService
@property
def image_url(self) -> str | None:
if not self.image:
return None
return CloudflareImagesService().get_url(self.image, "public")
For private images, use get_signed_url() instead:
return CloudflareImagesService().get_signed_url(self.image, "public")
For basic troubleshooting, check your browser's console or open an issue.
Contributing
See CONTRIBUTING.md for development setup, tests, and packaging.
Metadata
Release files for django-cloudflare-images 1.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| django_cloudflare_images-1.2.0.tar.gz | 18.0 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_cloudflare_images-1.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 31.0 kB
Release files / django_cloudflare_images-1.2.0.tar.gz
| Download URL | django_cloudflare_images-1.2.0.tar.gz |
|---|---|
| Size | 18.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
eac34084935127f399ae9190bf68f2bcb1ecac9bdb9b29fca6840bfd13c713f2
|
|
BLAKE2b-256 checksum How to use checksums |
01e08bc0b19d147bc571eb53547741562a655b5e65b5925aab159696ae004899
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Release files / django_cloudflare_images-1.2.0-py3-none-any.whl
| Download URL | django_cloudflare_images-1.2.0-py3-none-any.whl |
|---|---|
| Size | 13.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9f8c4489fd8096bf6649ddd62de140f4574c26574dac82700b68a8bd073c0c36
|
|
BLAKE2b-256 checksum How to use checksums |
26d24618e910626d7c5885f8d6e6358f6d4b3f3707f1ee6f59370fe67bda5c12
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.8
|
Security for django-cloudflare-images 1.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.