Coverage for baibu/core/storage_config.py: 100%
25 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-10-06 13:07 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-10-06 13:07 +0000
1"""Build the ``STORAGES`` setting from the environment.
3Kept free of Django imports so the settings module can call it.
5Two file stores are configured besides static files:
7- ``default``: ordinary uploads (Django's ``MEDIA``).
8- ``private``: contributor files (submitted text, audio, translations). They
9 are never given a public URL; code reads them through
10 :mod:`baibu.core.storage`.
12With ``STORAGE_BACKEND=local`` both live on the local filesystem. With
13``STORAGE_BACKEND=s3`` both live in one bucket on any S3-compatible server,
14under the ``media/`` and ``private/`` prefixes.
15"""
17from dataclasses import dataclass
19STORAGE_BACKENDS = ("local", "s3")
22class StorageConfigError(ValueError):
23 """Raised when the storage settings are incomplete or contradictory."""
26@dataclass(frozen=True)
27class S3Options:
28 bucket: str = ""
29 endpoint_url: str = ""
30 region: str = ""
31 access_key_id: str = ""
32 secret_access_key: str = ""
33 addressing_style: str = "path"
34 signed_url_seconds: int = 3600
37def build_storages(
38 backend: str,
39 *,
40 media_root: str,
41 private_root: str,
42 s3: S3Options | None = None,
43 staticfiles_backend: str = "django.contrib.staticfiles.storage.StaticFilesStorage",
44) -> dict:
45 staticfiles = {"BACKEND": staticfiles_backend}
46 if backend == "local":
47 return {
48 "default": {
49 "BACKEND": "django.core.files.storage.FileSystemStorage",
50 "OPTIONS": {"location": media_root},
51 },
52 "private": {
53 "BACKEND": "baibu.core.storage.PrivateFileSystemStorage",
54 "OPTIONS": {"location": private_root},
55 },
56 "staticfiles": staticfiles,
57 }
58 if backend == "s3":
59 s3 = s3 or S3Options()
60 if not s3.bucket:
61 msg = "STORAGE_BACKEND=s3 needs S3_BUCKET."
62 raise StorageConfigError(msg)
63 options = {
64 "bucket_name": s3.bucket,
65 "endpoint_url": s3.endpoint_url or None,
66 "region_name": s3.region or None,
67 "access_key": s3.access_key_id or None,
68 "secret_key": s3.secret_access_key or None,
69 "addressing_style": s3.addressing_style or None,
70 # Objects inherit the bucket's access policy; nothing is made public.
71 "default_acl": None,
72 "querystring_auth": True,
73 "querystring_expire": s3.signed_url_seconds,
74 # Never silently rename: callers choose unique keys.
75 "file_overwrite": False,
76 }
77 return {
78 "default": {"BACKEND": "storages.backends.s3.S3Storage", "OPTIONS": {**options, "location": "media"}},
79 "private": {"BACKEND": "storages.backends.s3.S3Storage", "OPTIONS": {**options, "location": "private"}},
80 "staticfiles": staticfiles,
81 }
82 msg = f"Unknown STORAGE_BACKEND {backend!r}; expected one of {', '.join(STORAGE_BACKENDS)}."
83 raise StorageConfigError(msg)