# Migrare MinIO: local multi-bucket → cluster extern single-bucket **Autor**: refactor 2026-04-25 **Branch**: `feat/minio-cluster-single-bucket` **Scop**: mutarea fișierelor user-uploaded de pe `staging-dataLayer-minio` (bucket-per-user) pe cluster extern managed (``, single bucket `didi-prod` cu prefix-uri). --- ## De ce single-bucket Cluster extern (4 noduri MinIO `minio1..4.`, EC:2, 466 GiB utilizabili) ne dă credentiale `didi-prod` cu **`s3:*` doar pe bucket-ul `didi-prod`**. Nu putem crea bucket-uri (testat: 3/3 încercări `Access Denied`). Soluția: păstrăm conceptul "namespace per user" dar ca **prefix** în bucket-ul fix: ``` ÎNAINTE (local, multi-bucket): DUPĂ (cluster, single-bucket): user-3/ didi-prod/ images/ users/3/ videos/ images/ audio-files/ videos/ user-19/ audio-files/ images/ users/19/ uploads/ images/ image-files/ didi-prod/uploads/ ← legacy fallback didi-prod/image-files/ ← legacy fallback ``` Numele de prefix-uri sistem (`uploads`, `image-files`, `audio-files`, `video-files`, `text-files`, `document-files`, `pipeline-artifacts`) sunt **identice cu numele bucket-urilor vechi** — astfel URL-urile vechi rămân interpretabile de proxy fără DB rewrite. --- ## Compatibilitate URL — backward compat URL-urile vechi din `bos_analysis.analysis_session.input_media_url` și PG history pointează la formate vechi: ``` https://didi365.eu/storage/user-3/images/abc.jpg ← format vechi user https://didi365.eu/api/v3/media/file/user-3/images/abc.jpg ← proxy vechi http://staging-dataLayer-minio:9000/image-files/foo.jpg ← bucket sistem ``` Codul rezolvă toate aceste forme la canonic `didi-prod/`: | URL primit | Resolved bucket | Resolved key | |---|---|---| | `user-3/images/abc.jpg` | `didi-prod` | `users/3/images/abc.jpg` | | `image-files/foo.jpg` | `didi-prod` | `image-files/foo.jpg` | | `didi-prod/users/3/images/abc.jpg` | `didi-prod` | `users/3/images/abc.jpg` (passthrough) | Implementat în: - `didiFramework/src/config/minio.ts` → `resolveBucketRequest(bucket, key)` - `agent-v3/src/shared/media/media-service.ts` → inline în `proxyFile()` --- ## Modificări în cod (branch `feat/minio-cluster-single-bucket`) ### didiFramework | Fișier | Schimbare | |---|---| | `src/config/minio.ts` | Refactor complet: `BUCKET` fix, `resolveBucketRequest()`, `userObjectKey()`, no-op `ensureBucket()` în cluster mode, `createUserBucket()` lazy (doar logging) | | `src/routes/auth.ts` (endpoint `/internal/get-bucket-info`) | Returnează `bucketName=didi-prod` + `folder=users/{id}/{mimeFolder}` în loc de `bucketName=user-{id}` + `folder={mimeFolder}` | | `src/routes/uploads.ts` | **Nemodificat** — funcționează prin backward compat (caller pasează `bucket=image-files`, layer-ul îl rezolvă) | | `sql/migrations/010_add_user_storage_quota.sql` | NEW — adaugă `storage_used_bytes` + `storage_limit_bytes` la `internet_user` (înlocuiește bucket tags) | | `docker-compose.yml` | Env vars cu fallback `${MINIO_*:-default}` | ### agent-v3 | Fișier | Schimbare | |---|---| | `src/shared/media/media-service.ts` | `ensureBucket()` no-op în single-bucket mode. `proxyFile()` rezolvă legacy bucket → cluster prefix. Ownership check acceptă atât `bucket=user-{N}` cât și prefix `users/{N}/`. `uploadFile()` folosește `MINIO_BUCKET` env ca fallback | | `docker-compose.yml` | `MINIO_BUCKET`, `MINIO_USE_SSL`, `MINIO_PUBLIC_ENDPOINT` adăugate la `x-worker-env` cu fallback empty (= legacy multi-bucket) | ### Scripts | Fișier | Schimbare | |---|---| | `scripts/minio-switch.sh` | NEW — swap între local/cluster. Sourceaza credentiale din `.cluster-credentials.env` | | `scripts/.cluster-credentials.env` | + `DIDI_MINIO_ACCESS_KEY` / `DIDI_MINIO_SECRET_KEY` / `DIDI_MINIO_DEV_*` | --- ## Cluster extern — date de conectare ``` S3 API: http://:9000 (VIP 10.11.10.128) Console: http://:9001 (VIP 10.11.10.129) Region: us-east-1 Path style: REQUIRED (forcePathStyle în SDK) TLS: false acum (HTTPS în curând prin HAProxy) User PROD: didi-prod / 627074a6ceb8d8531feffaf4ee9cc45007350c58 User DEV: didi-dev / 6f51811a4a157cb8f8d0d4376877552b780a06b8 Bucket PROD: didi-prod (full s3:* doar pe bucket-ul propriu) Bucket DEV: didi-dev (idem) ``` Infrastructură: 4 noduri Proxmox extern (`minio1..4.`, IPs `10.11.10.124-127`), erasure coding EC:2, HAProxy + keepalived VRRP, 466 GiB utilizabili. --- ## Cutover plan — NEEXECUTAT încă Codul e gata pe branch, **runtime-ul production rămâne pe local MinIO** până când executăm cutover-ul deliberat. ### Pre-cutover checklist 1. ✓ Backup complet creat: `/home/admin365/backups/pre-minio-refactor-20260425-0935/` (545 MB cu cod + git bundle + 476 MB MinIO data) 2. ✓ Branch `feat/minio-cluster-single-bucket` cu refactor + tests 3. ✓ Cluster `didi-dev` validat (write/list/delete cu prefix-uri noi) 4. ⏳ Apply migration 010 pe PG cluster 5. ⏳ Smoke test cu un user fictiv pe `didi-dev` (upload + analiză + URL public) ### Cutover steps ```bash # 1. Apply DB migration docker exec didi-framework node -e " const fs = require('fs'); const { Pool } = require('pg'); const pool = new Pool({ host: '10.11.50.167', port: 5000, user: 'bos_interface', password: 'interface', database: 'DIDI' }); const sql = fs.readFileSync('/app/sql/migrations/010_add_user_storage_quota.sql', 'utf8'); pool.query(sql).then(() => console.log('migration 010 applied')).catch(console.error).finally(() => pool.end()); " # 2. Mirror data local → cluster (~2 min pe LAN) mc alias set local http://staging-dataLayer-minio:9000 minioadmin minio123 mc alias set prod http://:9000 didi-prod '' # Per-user buckets → users/{id}/ prefix for u in $(mc ls local/ | grep -oE 'user-[0-9]+/' | tr -d '/'); do id=${u#user-} echo "mirroring $u → users/$id/" mc mirror local/$u prod/didi-prod/users/$id/ done # Sistem buckets → identical names ca prefix top-level for b in uploads image-files audio-files video-files text-files document-files pipeline-artifacts backups; do mc mirror local/$b prod/didi-prod/$b/ done # 3. Switch env la cluster ./backend/services/orchestration-layer/scripts/minio-switch.sh cluster # (rescrie .env, restart didi-framework + agent-v3) # 4. Smoke test curl -X POST https://didi365.eu/api/v3/pipeline/analyze ... # 5. Monitor 24-48h. Local stays running ca fallback. ``` ### Rollback (30s) ```bash ./backend/services/orchestration-layer/scripts/minio-switch.sh local ``` Local container rămâne pornit și păstrează datele. Singura pierdere: upload-uri făcute între cutover și rollback nu sunt pe local. Acceptabil pentru fereastra de monitoring. --- ## Riscuri cunoscute 1. **Quota** — la cutover, `storage_used_bytes` în PG e populat din migration 010 doar dacă userul are deja subscription cu `storage_limit_gb`. Pentru calcul real folosit, după cutover rulez: ```sql -- Reconciliere usage cu MinIO real (job periodic recomandat) SELECT internet_user_id FROM bos_sysadmin.internet_user WHERE storage_used_bytes = 0; -- Pentru fiecare → mc du didi-prod/users/{id}/ → UPDATE ``` 2. **TLS în curând** — endpoint va deveni `https://` (Let's Encrypt prin HAProxy). La acel moment: `MINIO_USE_SSL=true` + endpoint nou. 3. **Cleanup local cluster** — local `staging-dataLayer-minio` are 7 alte proiecte (lege365-corpus, ml-models, voice-recordings, publisher-assets, rafai-prod). NU le ștergem la cleanup. 4. **HAProxy failover** — VIP `10.11.10.128` rulează pe `cai1`. La failover poate fi o pauză de 2-5s. SDK-ul minio-js are retry implicit, dar la upload-uri mari (>100MB) merită setări explicite. --- ## Status build ``` ✓ tsc didiFramework — exit 0 ✓ tsc agent-v3 — zero erori noi (doar pre-existente pe `pg` module) ✓ docker compose build — toate imagini OK ✓ minio-switch.sh status — funcțional ✓ Test write/list/delete pe didi-dev cu prefix-uri noi — validat ```