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.2.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.2.0
File Size Uploaded Provenance
django_cloudflare_images-1.2.0.tar.gz 18.0 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for django-cloudflare-images 1.2.0
File Interpreter ABI Platform
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.

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