django-capo-s3
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 storages —
S3Storagefor media, plusS3StaticStorage(plain) andS3ManifestStaticStorage(content-hashed names +staticfiles.jsonmanifest for cache-busting) forcollectstatic. The manifest can live in the bucket or in a separate storage. - URLs — presigned URLs by default (with per-call
expireand response overrides likeresponse_content_disposition), unsigned public URLs, or CloudFront-signed URLs for a custom domain. - Uploads — streaming single-
PUTuploads 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"])
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=...
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",
},
}
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_capo_s3-0.0.3.tar.gz.
File metadata
- Download URL: django_capo_s3-0.0.3.tar.gz
- Upload date:
- Size: 18.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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a790367834558b92f1745604368528fab07296993e39b46396944563e7cb10c6
|
|
| MD5 |
50fcb907e4056558f0304e66a97d20c9
|
|
| BLAKE2b-256 |
40cb33ff55121624bd8cab8a2600587da4234907edbacbf7b25bbb1d39ebd761
|
File details
Details for the file django_capo_s3-0.0.3-py3-none-any.whl.
File metadata
- Download URL: django_capo_s3-0.0.3-py3-none-any.whl
- Upload date:
- Size: 19.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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
da1ad6d9b178cbcc2e315509d2aa4593b8557a4fea909ec56ca9f07f9927570a
|
|
| MD5 |
c42a88adc294fcebd5be6acc54e175cd
|
|
| BLAKE2b-256 |
4937afd6031afd5fb82e78f3196f2242dba0bd06a115985a1c5173366b555d35
|