Skip to main content

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.url returns a signed URL;
  • storage.open() and storage.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.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-cloudflare-images 1.3.0
File Size Uploaded Provenance Actions
django_cloudflare_images-1.3.0.tar.gz 21.4 kB Without provenance Details for django_cloudflare_images-1.3.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for django-cloudflare-images 1.3.0
File Interpreter ABI Platform Actions
django_cloudflare_images-1.3.0-py3-none-any.whl Python 3 none any Details for django_cloudflare_images-1.3.0-py3-none-any.whl

Total release size: 37.0 kB

Release files / django_cloudflare_images-1.3.0.tar.gz

Download URL django_cloudflare_images-1.3.0.tar.gz
Size 21.4 kB
Tags Source
SHA-256 checksum
How to use checksums
3f4fee4ab5b18a2e2ba3250be5d57df1fd7447176120e0698f4dcea3821eda34
BLAKE2b-256 checksum
How to use checksums
3ccedbc1a311b3ab56654df9a91a7768ce049e8cdab68461b7a85e3c81fc6b8e
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.3.0-py3-none-any.whl

Download URL django_cloudflare_images-1.3.0-py3-none-any.whl
Size 15.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
94156b1e5c01d42523e8c63f43dac28f4bf1dc408347a35d9d272cd407186899
BLAKE2b-256 checksum
How to use checksums
c37210b34baae10ffb7a0149f31f270c7f7a080f0866837d05189fc66b5c6636
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.3.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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page