Skip to main content

django-capo-s3

PyPI - Python Version PyPI PyPI - Downloads CodSpeed Badge

A Django file storage backend for S3-compatible object stores, built on the capo-s3 client instead of boto3 — a drop-in alternative to django-storages[s3].

Features

  • Media and static storagesS3Storage for media, plus S3StaticStorage (plain) and S3ManifestStaticStorage (content-hashed names + staticfiles.json manifest for cache-busting) for collectstatic. The manifest can live in the bucket or in a separate storage.
  • URLs — presigned URLs by default (with per-call expire and response overrides like response_content_disposition), unsigned public URLs, or CloudFront-signed URLs for a custom domain.
  • Uploads — streaming single-PUT uploads that automatically switch to a concurrent multipart transfer above a configurable threshold.
  • Transparent gzip — eligible content types are stored compressed and decompressed on read.
  • Server-side encryption, storage class, cache-control, metadata, ... — via object_parameters, passed straight through to the underlying request.
  • Flexible networking options — custom endpoint (e.g. MinIO), path- or virtual-host addressing, TLS verification and custom CA bundles, connection timeouts, pool size, HTTP/HTTPS/SOCKS5 proxies, and retry attempts.
  • Fully typed — this is already the bare minimum for new packages.

Installation

uv add django-capo-s3   # or: pip install django-capo-s3

Configuration

Register the backend in Django's STORAGES setting; everything under OPTIONS is passed to the backend:

STORAGES = {
    "default": {
        "BACKEND": "django_capo_s3.S3Storage",
        "OPTIONS": {
            "bucket": "my-bucket",
            "region": "eu-central-1",
            "location": "media",
        },
    },
    "staticfiles": {
        "BACKEND": "django_capo_s3.S3ManifestStaticStorage",
        "OPTIONS": {"bucket": "my-bucket", "location": "static"},
    },
}

Usage

On a model field

Most apps never touch storage directly. With STORAGES["default"] configured (above), FileField / ImageField just work — uploads, .url, .size, and .open() all go through the backend.

from django.db import models

class Report(models.Model):
    csv = models.FileField(upload_to="reports/")

report = Report.objects.create(csv=uploaded_file)
report.csv.url            # presigned URL to the object
report.csv.size           # size in bytes
report.csv.open().read()  # file contents

Direct storage access

Grab the configured default storage from the registry and use the Storage API directly.

from django.core.files.base import ContentFile
from django.core.files.storage import storages

storage = storages["default"]
name = storage.save("reports/june.csv", ContentFile(b"col1,col2\n"))  # returns the stored name
storage.exists(name)          # True
storage.size(name)            # size in bytes
with storage.open(name) as f:
    data = f.read()
storage.delete(name)          # no error if it's already gone

Download URLs

url() is presigned by default. Override the lifetime per call, or add response headers — for example, to force a browser "Save as" with a filename.

storage.url("reports/june.csv")                # presigned, default lifetime (url_expire)
storage.url("reports/june.csv", expire=60)     # presigned, valid for 60 seconds
storage.url(
    "reports/june.csv",
    parameters={"response_content_disposition": 'attachment; filename="june.csv"'},
)

For a public bucket, set "querystring_auth": False in OPTIONS to get plain, cacheable URLs instead.

Bulk delete

delete_objects() removes many objects in a single bulk request (per 1000 keys). Missing keys are ignored, just like delete().

storage.delete_objects(["reports/jan.csv", "reports/feb.csv", "reports/mar.csv"])

Multiple regions

A storage is bound to one region, but for_region() returns a clone bound to another — cached per region, so repeated calls reuse the same client and connection pool. Handy for routing objects to a region at runtime, for example from a model's FileField.

storage = storages["default"]
storage.for_region("sa-east-1").save("br/report.csv", content)   # store in São Paulo
storage.for_region("ap-southeast-2").save("au/report.csv", content)  # store in Sydney

class Report(models.Model):
    region = models.CharField(max_length=20)
    csv = models.FileField(upload_to="reports/")

    def save(self, *args, **kwargs):
        self.csv.storage = storages["default"].for_region(self.region)
        super().save(*args, **kwargs)

Static files with cache-busting

Point STORAGES["staticfiles"] at S3ManifestStaticStorage. collectstatic then stores each file under a content-hashed name and {% static %} resolves through the manifest, so assets can be served with long-lived caching. Keep the manifest local so web workers don't fetch it from S3 on startup.

from django.core.files.storage import FileSystemStorage

STORAGES["staticfiles"] = {
    "BACKEND": "django_capo_s3.S3ManifestStaticStorage",
    "OPTIONS": {
        "bucket": "my-bucket",
        "location": "static",
        "manifest_storage": FileSystemStorage(location=BASE_DIR / ".static-manifest"),
    },
}

Faster re-deploys. During collectstatic, the hashing pass lists the bucket once and skips uploading any hashed asset whose content is already stored — so an unchanged deploy costs no uploads instead of re-uploading every file. This is on by default ("skip_unchanged": True); set it to False to fall back to Django's behaviour (for example, on an S3-compatible store whose ETag isn't a content MD5).

Serving through a CDN (CloudFront)

Set custom_domain for plain CDN URLs, or add a CloudFront key pair to sign them for a private distribution.

"OPTIONS": {
    "bucket": "my-bucket",
    "custom_domain": "d123.cloudfront.net",
    "cloudfront_key": cloudfront_private_key_pem,  # PEM contents
    "cloudfront_key_id": "K1ABCDEF",
    "url_expire": 300,
}
# storage.url(name) -> https://d123.cloudfront.net/...?Expires=...&Signature=...&Key-Pair-Id=...

Response overrides passed to url() are signed into the CloudFront URL, so they can't be tampered with — configure the distribution to forward them to the S3 origin for them to take effect:

storage.url("report.pdf", parameters={"response_content_disposition": 'attachment; filename="report.pdf"'})

Large uploads and gzip

Uploads switch to a concurrent multipart transfer above multipart_threshold; text assets can be stored gzip-compressed and are transparently decompressed on read.

"OPTIONS": {
    "bucket": "my-bucket",
    "multipart_threshold": 32 * 1024 * 1024,  # start multipart at 32 MiB
    "multipart_chunksize": 16 * 1024 * 1024,
    "multipart_concurrency": 8,               # parts uploaded in parallel
    "gzip": True,                             # compress CSS/JS/JSON/... at rest
}

Encryption, storage class, and other object metadata

Whatever object_parameters contains is passed straight to each upload — e.g. SSE-KMS plus a storage class and cache header.

"OPTIONS": {
    "bucket": "my-bucket",
    "default_acl": "private",
    "object_parameters": {
        "server_side_encryption": "aws:kms",
        "ssekms_key_id": "arn:aws:kms:eu-central-1:123456789012:key/abcd-...",
        "storage_class": "STANDARD_IA",
        "cache_control": "max-age=86400",
    },
}

Self-hosted stores (MinIO) with a separate public host

When the client reaches the store at an internal endpoint (say http://minio:9000 inside Docker) but browsers must use a different public host, set custom_domain to that public host. With force_path_style the bucket goes into the URL path, matching how path-style S3 serves it.

"OPTIONS": {
    "bucket": "my-bucket",
    "endpoint": "http://minio:9000",   # where the app connects
    "custom_domain": "localhost:9000",  # where browsers connect
    "force_path_style": True,
    "url_protocol": "http",
    "querystring_auth": False,
}
# storage.url("photo.jpg") -> http://localhost:9000/my-bucket/photo.jpg

Networking (endpoint tuning, proxies)

Tune timeouts, the connection pool, retries, TLS verification, and proxies as needed.

"OPTIONS": {
    "bucket": "my-bucket",
    "connect_timeout": 5.0,
    "read_timeout": 30.0,
    "max_connections_per_host": 50,
    "retry_max_attempts": 5,
    "verify": "/etc/ssl/certs/internal-ca.pem",   # or False to disable TLS verification
    "proxies": {"https": "http://proxy.internal:8080"},
}

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_capo_s3-0.1.0.tar.gz (19.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_capo_s3-0.1.0-py3-none-any.whl (20.6 kB view details)

Uploaded Python 3

File details

Details for the file django_capo_s3-0.1.0.tar.gz.

File metadata

  • Download URL: django_capo_s3-0.1.0.tar.gz
  • Upload date:
  • Size: 19.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for django_capo_s3-0.1.0.tar.gz
Algorithm Hash digest
SHA256 4ff45d64ce74719ca997ea120705c63c0fb78ff7064962721c2c4e872c29fb78
MD5 abb1a53062e134f8907ca02230b50ac0
BLAKE2b-256 7c58dbf8bd5ca41fdf0aa1efc07c677438bfe1ed22411630febe6182dd91f697

See more details on using hashes here.

File details

Details for the file django_capo_s3-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: django_capo_s3-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 20.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for django_capo_s3-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 12eb36ebdcda6cbab2a1c69f6537bdb574ad45dc3cf25f87bb473af22c44173e
MD5 59bfa6ef40a547e9b0faadd7fc5bd3d4
BLAKE2b-256 770b374a1621b21654ebfe6951195c0788b51bf0946552abb6b8979d6add1201

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page