From c4f15d9c801d345d815fb308e9d3113728051b29 Mon Sep 17 00:00:00 2001 From: Ali Asadi Date: Mon, 17 Aug 2026 18:01:29 +0330 Subject: [PATCH] Integrate MinIO for media storage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .env.example | 12 ++++++++++++ CLAUDE.md | 1 + config/settings.py | 45 ++++++++++++++++++++++++++++++++++++++++++++- docker-compose.yml | 16 ++++++++++++++++ requirements.in | 4 ++++ requirements.txt | 5 +++++ 6 files changed, 82 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index cd005e5..6c6bb66 100644 --- a/.env.example +++ b/.env.example @@ -14,6 +14,18 @@ CSRF_TRUSTED_ORIGINS= NEARBY_RADIUS_METERS=5000 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 # service's own client-credentials grant for outbound calls to Gooyal services. OAUTH2_PROVIDER_BASE_PUBLIC_URL= diff --git a/CLAUDE.md b/CLAUDE.md index 364ce4a..bb15c56 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,6 +44,7 @@ Key `.env` variables (see `.env.example`): - `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) - `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 diff --git a/config/settings.py b/config/settings.py index 1e93470..5d83fdb 100644 --- a/config/settings.py +++ b/config/settings.py @@ -1,3 +1,4 @@ +from datetime import timedelta from pathlib import Path import environ @@ -28,6 +29,7 @@ INSTALLED_APPS = [ 'rest_framework_gis', 'django_filters', 'corsheaders', + 'django_minio_backend.apps.DjangoMinioBackendConfig', # local apps 'apps.core', @@ -137,9 +139,50 @@ STATIC_URL = "{}static/".format(API_BASE_PATH) STATIC_ROOT = BASE_DIR / "static" MEDIA_URL = "{}media/".format(API_BASE_PATH) 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 = { "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": { "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", diff --git a/docker-compose.yml b/docker-compose.yml index 2e966b5..b551f61 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,7 @@ services: depends_on: - db - redis + - minio networks: - app - monitoring @@ -32,6 +33,20 @@ services: networks: - 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: image: grafana/promtail:2.8.0 volumes: @@ -43,6 +58,7 @@ services: volumes: pgdata: + miniodata: networks: app: diff --git a/requirements.in b/requirements.in index e188981..f754835 100644 --- a/requirements.in +++ b/requirements.in @@ -41,6 +41,10 @@ python-dateutil==2.9.0.post0 whitenoise==6.9.0 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 gunicorn==26.0.0 gevent==26.7.0 diff --git a/requirements.txt b/requirements.txt index 5337fef..9931cde 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ anyio==4.14.2 +argon2-cffi==25.1.0 +argon2-cffi-bindings==25.1.0 asgiref==3.11.1 attrs==25.4.0 certifi==2026.7.22 @@ -9,6 +11,7 @@ Django==6.0.2 django-cors-headers==4.9.0 django-environ==0.13.0 django-filter==25.2 +django-minio-backend==4.5.0 django-oauth-toolkit==3.2.0 django-redis==6.0.0 djangorestframework==3.16.1 @@ -23,12 +26,14 @@ inflection==0.5.1 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 jwcrypto==1.5.8 +minio==7.2.20 oauthlib==3.3.1 pillow==12.3.0 psycopg==3.3.3 psycopg-binary==3.3.3 psycopg-pool==3.3.1 pycparser==3.0 +pycryptodome==3.23.0 python-dateutil==2.9.0.post0 python-decouple==3.8 PyYAML==6.0.3