Configuration¶
Register a backend in Django's STORAGES setting. Everything under OPTIONS is passed to the storage, and
only bucket is required:
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"},
},
}
Where a value comes from¶
Each option is resolved in this order, first hit winning:
- the storage's own
OPTIONS; - a matching
AWS_*setting, for compatibility with django-storages; - the built-in default.
OPTIONS is the recommended place — it keeps two storages pointing at different buckets independent, which a
global setting cannot. The settings layer exists so a project migrating off django-storages doesn't have to
move its configuration in the same change as the backend swap.
Credentials are the exception worth stating plainly: leave them unset and capo resolves them the way the AWS
tooling does — environment variables, ~/.aws/credentials, a container role, IRSA, SSO. You only need the
credentials option (or the key settings below) when the keys live somewhere capo won't look.
Settings read for django-storages compatibility¶
| Setting | Option |
|---|---|
AWS_STORAGE_BUCKET_NAME |
bucket |
AWS_LOCATION |
location |
AWS_S3_ENDPOINT_URL |
endpoint |
AWS_S3_REGION_NAME |
region |
AWS_S3_ADDRESSING_STYLE |
force_path_style ("path" → True) |
AWS_QUERYSTRING_AUTH |
querystring_auth |
AWS_QUERYSTRING_EXPIRE |
url_expire |
AWS_S3_CUSTOM_DOMAIN |
custom_domain |
AWS_S3_URL_PROTOCOL |
url_protocol (the trailing colon is dropped) |
AWS_CLOUDFRONT_KEY, AWS_CLOUDFRONT_KEY_ID |
cloudfront_key, cloudfront_key_id |
AWS_S3_FILE_OVERWRITE |
file_overwrite |
AWS_DEFAULT_ACL |
default_acl |
AWS_IS_GZIPPED |
gzip |
GZIP_CONTENT_TYPES |
gzip_content_types |
AWS_S3_OBJECT_PARAMETERS |
object_parameters |
AWS_S3_MAX_MEMORY_SIZE |
max_memory_size |
AWS_S3_SESSION_PROFILE |
session_profile |
AWS_S3_PROXIES |
proxies |
AWS_S3_VERIFY |
verify (None means "use the default") |
AWS_S3_ACCESS_KEY_ID or AWS_ACCESS_KEY_ID |
credentials (with the secret below) |
AWS_S3_SECRET_ACCESS_KEY or AWS_SECRET_ACCESS_KEY |
credentials |
AWS_SESSION_TOKEN or AWS_SECURITY_TOKEN |
credentials |
Both halves of a key pair have to be present before credentials is built at all — a lone access key is
ignored rather than half-configuring the client, so the ambient chain still gets its chance.
The static storages pin file_overwrite=True and querystring_auth=False regardless of these settings.
Hashed asset names are meant to be overwritten in place and served unsigned, so those two aren't negotiable.
AWS_S3_SIGNATURE_VERSION is not read: capo signs with SigV4 only. Neither are AWS_S3_USE_SSL (put the
scheme in endpoint), AWS_S3_FILE_NAME_CHARSET, AWS_S3_TRANSFER_CONFIG, AWS_S3_CLIENT_CONFIG or
AWS_S3_USE_THREADS — see Further optimizations for the options that replace the
last three.
Options with no django-storages equivalent¶
skip_unchanged, default_content_type, multipart_threshold, multipart_chunksize,
multipart_concurrency, connect_timeout, read_timeout, write_timeout, total_timeout,
max_connections_per_host, retry_max_attempts, http_client_builder and http_handler are set through
OPTIONS only.
Storing bytes under a known content type¶
The storage reads content_type off the file it is handed, but Django only puts that attribute on uploaded
files — a plain ContentFile has none. TypedContentFile closes that gap, which matters most for keys with no
extension to guess from:
from django_capo_s3 import TypedContentFile
storage.save(f"statements/{statement.id}", TypedContentFile(pdf_bytes, content_type="application/pdf"))
Deriving the name from the content¶
Override object_name(name, content) to decide the stored name from the content itself — normalizing an
extension, say, or using a content-addressed name. Whatever it returns becomes both the object key and the name
Django records on the model field, so the two can't drift apart.
import mimetypes
from django_capo_s3 import S3Storage
class ImageStorage(S3Storage):
def object_name(self, name, content):
if "." in name.rsplit("/", 1)[-1]:
return name
extension = mimetypes.guess_extension(getattr(content, "content_type", "") or "")
return f"{name}{extension}" if extension else name
The content is rewound before and after the call, so an override may read it freely to sniff a type.
Django resolves name collisions before this runs, so a name produced here isn't checked against the bucket:
with file_overwrite off, returning the name of an existing object still overwrites it.
One default to check when migrating¶
max_memory_size defaults to 10 MB, where django-storages uses 0. An opened file is held in memory up to
that size before spilling to a temporary file, so on a read-heavy path this is real memory per open file. Set
it to 0 to match django-storages and always spill to disk.