Files
where_woof/openspec/changes/photo-object-storage/design.md

3.2 KiB

Context

Phase 3 added local photo uploads to static/uploads/. Phase 6 moves them to MinIO (S3-compatible, self-hosted on .13) so they survive deploys and the admin can manage them. Both apps run on .13 alongside MinIO → no new firewall ports (the Go app serves photos through :3020, already open).

Goals / Non-Goals

Goals:

  • Uploads go to MinIO; public page serves them via the Go app (/photos/…).
  • Admin can upload photos to the same bucket (Filament S3 disk).
  • Existing uploads migrated; external photo URLs keep working.

Non-Goals:

  • Public bucket exposure (photos served through the app, not MinIO directly).
  • Image resizing/optimisation (future).
  • Cloud S3 (this is self-hosted; swapping to AWS S3 later = same S3 API).

Decisions

  1. MinIO on .13 at /home/sam/Docker/Containers/wherewoof-minio/ (compose: minio/minio server, API 9000, console 9001, volume wherewoof-minio-data; creds in a local .env, chmod 600). Create bucket wherewoof at startup (entrypoint mc alias + mb, or a one-off).
  2. Go storage via minio-go/v7: UploadPhoto reads the multipart file → PutObject to bucket wherewoof, key tags/{id}.{ext} → sets photo_url = /photos/tags/{id}.{ext}. Serve route GET /photos/{key...}GetObject → stream with Content-Type + Cache-Control: public, max-age=86400.
  3. Config: MINIO_ENDPOINT (host:port), MINIO_ACCESS_KEY, MINIO_SECRET_KEY, MINIO_BUCKET (default wherewoof), env-driven; log-mode/no-op if unset (dev keeps working via the existing local fallback? — no: unset → upload returns a friendly error; local file upload is replaced). Decision: if MINIO_* unset, UploadPhoto responds "storage not configured" (dev DB tests unaffected — they don't upload).
  4. Admin S3 disk: composer require league/flysystem-aws-s3-v3; config/filesystems.php adds minio disk (driver s3, endpoint http://minio:9000 from the admin container — via docker network or host IP 192.168.20.13:9000, use_ssl false); TagResource photo → FileUpload::make('photo_url')->disk('minio') storing to tags/ and setting the URL via the frontend domain (https://where-woof.com/photos/…).
  5. Migration: one-off script copies static/uploads/* objects into MinIO with matching keys (dev data; run once).

Risks / Trade-offs

  • [MinIO creds in .env files] → chmod 600, gitignored, consistent with existing pattern.
  • [Serving through the app adds a hop] → fine (same host, localhost); gives auth/cache control later.
  • [Admin image preview needs the frontend URL] → <img> cross-origin is fine; uses the public frontend domain.
  • [flysystem package version drift with Filament] → pin as Filament requires (uses its own storage config).

Migration Plan

  1. Provision MinIO on .13 (compose + env + bucket); verify with mc/curl.
  2. Go: add minio-go, UploadPhoto→S3, /photos/ route, config; build + verify suite (upload → served).
  3. Admin: s3 disk + Filament upload; verify via admin UI (browser).
  4. One-off migrate static/uploads → MinIO.
  5. Deploy: frontend env + binary; admin .env + rebuild container. Verify live photo round-trip.

Open Questions

  • Whether to keep the local static/uploads fallback for dev (decided: no — MinIO only; dev can point MINIO_ENDPOINT at the .13 instance).