Integrate MinIO for media storage

Swaps default media storage from local FileSystemStorage to MinIO via
django-minio-backend, matching the rest of the Winsoo ecosystem
(advertising, campaign, wallet). Points at a local MinIO container
(added to docker-compose.yml) by default since no shared bucket is
provisioned yet. Existing ImageFields pick up the new storage backend
automatically — no model changes needed.

Verified against a live local MinIO container: bucket auto-creates on
first save, object lands under the expected key, URL resolves.
This commit is contained in:
Ali Asadi 2026-08-17 18:01:29 +03:30
parent c9b617483d
commit c4f15d9c80
6 changed files with 82 additions and 1 deletions

View file

@ -14,6 +14,18 @@ CSRF_TRUSTED_ORIGINS=
NEARBY_RADIUS_METERS=5000 NEARBY_RADIUS_METERS=5000
DEFAULT_COMMISSION_PERCENT=4.0 DEFAULT_COMMISSION_PERCENT=4.0
# MinIO — object storage for media (product/store images), same pattern as the
# rest of the Winsoo ecosystem. Defaults point at the local `minio` container
# from docker-compose.yml. No bucket has been provisioned centrally yet —
# django-minio-backend will create MINIO_MEDIA_FILES_BUCKET on first save.
MINIO_ENDPOINT=localhost:9000
MINIO_USE_HTTPS=False
MINIO_EXTERNAL_ENDPOINT=localhost:9000
MINIO_EXTERNAL_ENDPOINT_USE_HTTPS=False
MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=minioadmin
MINIO_MEDIA_FILES_BUCKET=winofy-media
# OAuth2 provider (Gooyal accounts) — resource-server introspection + this # OAuth2 provider (Gooyal accounts) — resource-server introspection + this
# service's own client-credentials grant for outbound calls to Gooyal services. # service's own client-credentials grant for outbound calls to Gooyal services.
OAUTH2_PROVIDER_BASE_PUBLIC_URL= OAUTH2_PROVIDER_BASE_PUBLIC_URL=

View file

@ -44,6 +44,7 @@ Key `.env` variables (see `.env.example`):
- `ALLOWED_HOSTS`, `CORS_ALLOWED_ORIGINS`, `CSRF_TRUSTED_ORIGINS` — comma-separated lists - `ALLOWED_HOSTS`, `CORS_ALLOWED_ORIGINS`, `CSRF_TRUSTED_ORIGINS` — comma-separated lists
- `OAUTH2_PROVIDER_*` — Gooyal accounts OAuth2 resource-server + this app's own client credentials (see below) - `OAUTH2_PROVIDER_*` — Gooyal accounts OAuth2 resource-server + this app's own client credentials (see below)
- `DEFAULT_COMMISSION_PERCENT` — platform commission on delivered orders (overridable per `Store`) - `DEFAULT_COMMISSION_PERCENT` — platform commission on delivered orders (overridable per `Store`)
- `MINIO_*` — MinIO object storage for media (`STORAGES['default']`), same pattern as the rest of the Winsoo ecosystem; defaults target the local `minio` container in `docker-compose.yml`
## Authentication — Gooyal accounts, resource-server pattern ## Authentication — Gooyal accounts, resource-server pattern

View file

@ -1,3 +1,4 @@
from datetime import timedelta
from pathlib import Path from pathlib import Path
import environ import environ
@ -28,6 +29,7 @@ INSTALLED_APPS = [
'rest_framework_gis', 'rest_framework_gis',
'django_filters', 'django_filters',
'corsheaders', 'corsheaders',
'django_minio_backend.apps.DjangoMinioBackendConfig',
# local apps # local apps
'apps.core', 'apps.core',
@ -137,9 +139,50 @@ STATIC_URL = "{}static/".format(API_BASE_PATH)
STATIC_ROOT = BASE_DIR / "static" STATIC_ROOT = BASE_DIR / "static"
MEDIA_URL = "{}media/".format(API_BASE_PATH) MEDIA_URL = "{}media/".format(API_BASE_PATH)
MEDIA_ROOT = BASE_DIR / "media" MEDIA_ROOT = BASE_DIR / "media"
# MinIO — same object-storage pattern as the rest of the Winsoo ecosystem
# (campaign, wallet, advertising, ...). Points at a local MinIO instance by
# default; no bucket has been provisioned centrally yet, so MINIO_MEDIA_FILES_BUCKET
# just needs to be set to whatever this service's bucket ends up being called.
MINIO_ENDPOINT = env("MINIO_ENDPOINT", default="localhost:9000")
MINIO_USE_HTTPS = env.bool("MINIO_USE_HTTPS", default=False)
MINIO_EXTERNAL_ENDPOINT = env("MINIO_EXTERNAL_ENDPOINT", default=MINIO_ENDPOINT)
MINIO_EXTERNAL_ENDPOINT_USE_HTTPS = env.bool("MINIO_EXTERNAL_ENDPOINT_USE_HTTPS", default=MINIO_USE_HTTPS)
MINIO_REGION = None
MINIO_ACCESS_KEY = env("MINIO_ACCESS_KEY", default="minioadmin")
MINIO_SECRET_KEY = env("MINIO_SECRET_KEY", default="minioadmin")
MINIO_URL_EXPIRY_HOURS = timedelta(days=1)
MINIO_CONSISTENCY_CHECK_ON_START = False
MINIO_MEDIA_FILES_BUCKET = env("MINIO_MEDIA_FILES_BUCKET", default="winofy-media") # replacement for MEDIA_ROOT
MINIO_PRIVATE_BUCKETS = [
'default',
]
MINIO_PUBLIC_BUCKETS = [
'default-public',
MINIO_MEDIA_FILES_BUCKET,
]
MINIO_POLICY_HOOKS = []
MINIO_BUCKET_CHECK_ON_SAVE = True # creates the bucket if missing, then saves
STORAGES = { STORAGES = {
"default": { "default": {
"BACKEND": "django.core.files.storage.FileSystemStorage", "BACKEND": "django_minio_backend.models.MinioBackend",
"OPTIONS": {
"MINIO_ENDPOINT": MINIO_ENDPOINT,
"MINIO_USE_HTTPS": MINIO_USE_HTTPS,
"MINIO_EXTERNAL_ENDPOINT": MINIO_EXTERNAL_ENDPOINT,
"MINIO_EXTERNAL_ENDPOINT_USE_HTTPS": MINIO_EXTERNAL_ENDPOINT_USE_HTTPS,
"MINIO_REGION": MINIO_REGION,
"MINIO_ACCESS_KEY": MINIO_ACCESS_KEY,
"MINIO_SECRET_KEY": MINIO_SECRET_KEY,
"MINIO_URL_EXPIRY_HOURS": MINIO_URL_EXPIRY_HOURS,
"MINIO_CONSISTENCY_CHECK_ON_START": MINIO_CONSISTENCY_CHECK_ON_START,
"MINIO_DEFAULT_BUCKET": MINIO_MEDIA_FILES_BUCKET,
"MINIO_PRIVATE_BUCKETS": MINIO_PRIVATE_BUCKETS,
"MINIO_PUBLIC_BUCKETS": MINIO_PUBLIC_BUCKETS,
"MINIO_POLICY_HOOKS": MINIO_POLICY_HOOKS,
"MINIO_BUCKET_CHECK_ON_SAVE": MINIO_BUCKET_CHECK_ON_SAVE,
},
}, },
"staticfiles": { "staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",

View file

@ -12,6 +12,7 @@ services:
depends_on: depends_on:
- db - db
- redis - redis
- minio
networks: networks:
- app - app
- monitoring - monitoring
@ -32,6 +33,20 @@ services:
networks: networks:
- app - app
minio:
image: minio/minio:latest
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:-minioadmin}
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:-minioadmin}
ports:
- "9000:9000"
- "9001:9001"
volumes:
- miniodata:/data
networks:
- app
promtail: promtail:
image: grafana/promtail:2.8.0 image: grafana/promtail:2.8.0
volumes: volumes:
@ -43,6 +58,7 @@ services:
volumes: volumes:
pgdata: pgdata:
miniodata:
networks: networks:
app: app:

View file

@ -41,6 +41,10 @@ python-dateutil==2.9.0.post0
whitenoise==6.9.0 whitenoise==6.9.0
Pillow==12.3.0 # ImageField support (product/store images) Pillow==12.3.0 # ImageField support (product/store images)
# Media storage — MinIO, same pattern as the rest of the Winsoo ecosystem
django-minio-backend==4.5.0
minio==7.2.20
# App server # App server
gunicorn==26.0.0 gunicorn==26.0.0
gevent==26.7.0 gevent==26.7.0

View file

@ -1,4 +1,6 @@
anyio==4.14.2 anyio==4.14.2
argon2-cffi==25.1.0
argon2-cffi-bindings==25.1.0
asgiref==3.11.1 asgiref==3.11.1
attrs==25.4.0 attrs==25.4.0
certifi==2026.7.22 certifi==2026.7.22
@ -9,6 +11,7 @@ Django==6.0.2
django-cors-headers==4.9.0 django-cors-headers==4.9.0
django-environ==0.13.0 django-environ==0.13.0
django-filter==25.2 django-filter==25.2
django-minio-backend==4.5.0
django-oauth-toolkit==3.2.0 django-oauth-toolkit==3.2.0
django-redis==6.0.0 django-redis==6.0.0
djangorestframework==3.16.1 djangorestframework==3.16.1
@ -23,12 +26,14 @@ inflection==0.5.1
jsonschema==4.26.0 jsonschema==4.26.0
jsonschema-specifications==2025.9.1 jsonschema-specifications==2025.9.1
jwcrypto==1.5.8 jwcrypto==1.5.8
minio==7.2.20
oauthlib==3.3.1 oauthlib==3.3.1
pillow==12.3.0 pillow==12.3.0
psycopg==3.3.3 psycopg==3.3.3
psycopg-binary==3.3.3 psycopg-binary==3.3.3
psycopg-pool==3.3.1 psycopg-pool==3.3.1
pycparser==3.0 pycparser==3.0
pycryptodome==3.23.0
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
python-decouple==3.8 python-decouple==3.8
PyYAML==6.0.3 PyYAML==6.0.3