openspec: photo-object-storage change — MinIO on .13, Go upload/serve, admin S3 disk
This commit is contained in:
2
openspec/changes/photo-object-storage/.openspec.yaml
Normal file
2
openspec/changes/photo-object-storage/.openspec.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-07
|
||||||
3
openspec/changes/photo-object-storage/README.md
Normal file
3
openspec/changes/photo-object-storage/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# photo-object-storage
|
||||||
|
|
||||||
|
Photos to MinIO object storage (.13): Go uploads/serves via S3, admin Filament S3 disk, photos survive redeploys
|
||||||
42
openspec/changes/photo-object-storage/design.md
Normal file
42
openspec/changes/photo-object-storage/design.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
## 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).
|
||||||
29
openspec/changes/photo-object-storage/proposal.md
Normal file
29
openspec/changes/photo-object-storage/proposal.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
Photos currently save to the app's local `static/uploads/` (Phase 3). That's lost on a fresh deploy or multi-instance setup, and the admin can't manage them. Phase 6 moves photos to **MinIO** (S3-compatible object storage) — a self-hosted Docker service on .13, consistent with the infra pattern and requiring no cloud dependency.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- **MinIO** container on .13 (API :9000, console :9001, volume-backed) with a `wherewoof` bucket; credentials in a local `.env` (never in the repo).
|
||||||
|
- **Go frontend**: `UploadPhoto` now PUTs the object to MinIO (`tags/{id}.{ext}`) and stores the serve URL (`/photos/tags/{id}.{ext}`) in `tags.photo_url`. A new `GET /photos/{key...}` route streams the object back (content-type + cache headers). External photo URLs still work unchanged.
|
||||||
|
- **Laravel admin**: S3 disk (flysystem) pointing at MinIO; Filament `FileUpload` on the Tag resource stores to the same bucket and sets `photo_url` via the frontend's `/photos/` URL.
|
||||||
|
- **Migration**: copy any existing `static/uploads/*` into MinIO (one-off script).
|
||||||
|
- **Deploy**: MinIO on .13 (Docker, same host as both apps — no new firewall ports), frontend binary + env (`MINIO_*`), admin `.env` S3 config.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `photo-object-storage`: MinIO-backed photo storage; uploads + serving via the Go app; photos persist across deploys.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `database`: none required (photo_url already exists).
|
||||||
|
- `tag-management` / admin: photo upload available in the Filament admin.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- **On .13**: `/home/sam/Docker/Containers/wherewoof-minio/` (compose + env + volume), bucket `wherewoof`.
|
||||||
|
- **Go**: `frontend/internal/handlers/tags.go` (UploadPhoto → MinIO), `photo.go` (serve route), `internal/config` (MINIO_* env), `go.mod` (+minio-go), `main.go` (route).
|
||||||
|
- **Admin**: `config/filesystems.php` s3 disk, `composer require league/flysystem-aws-s3-v3`, TagResource photo field.
|
||||||
|
- Env: `MINIO_ENDPOINT/ACCESS/SECRET/BUCKET` in `~/.config/where-woof.env` (.13) and `admin/.env`.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Photos stored in MinIO
|
||||||
|
Photo uploads SHALL be stored as objects in the MinIO bucket, keyed by tag (`tags/{id}.{ext}`), with `tags.photo_url` set to the app's serve path (`/photos/tags/{id}.{ext}`).
|
||||||
|
|
||||||
|
#### Scenario: Upload a photo
|
||||||
|
- **WHEN** the owner uploads an image on the edit page
|
||||||
|
- **THEN** the object is stored in MinIO and the tag's `photo_url` points at `/photos/…`
|
||||||
|
|
||||||
|
### Requirement: Photos served by the app
|
||||||
|
`GET /photos/{key...}` SHALL stream the object from MinIO with the correct content type and cache headers. External photo URLs SHALL continue to work.
|
||||||
|
|
||||||
|
#### Scenario: Public page shows the photo
|
||||||
|
- **WHEN** a visitor opens an active tag page with an uploaded photo
|
||||||
|
- **THEN** the image loads via `/photos/…`
|
||||||
|
|
||||||
|
#### Scenario: External URL photo
|
||||||
|
- **WHEN** `photo_url` is an external https URL
|
||||||
|
- **THEN** the page uses it directly (unchanged behaviour)
|
||||||
|
|
||||||
|
### Requirement: Photos survive redeploys
|
||||||
|
Photo objects SHALL persist independently of the app (MinIO volume), so a fresh deploy does not lose photos.
|
||||||
|
|
||||||
|
#### Scenario: Redeploy after upload
|
||||||
|
- **WHEN** the frontend is redeployed after a photo upload
|
||||||
|
- **THEN** the photo still loads (object lives in MinIO)
|
||||||
21
openspec/changes/photo-object-storage/tasks.md
Normal file
21
openspec/changes/photo-object-storage/tasks.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
## 1. MinIO Provisioning
|
||||||
|
|
||||||
|
- [ ] 1.1 Compose + env on .13 (`wherewoof-minio`, API 9000 / console 9001, volume); `docker compose up -d`; create bucket `wherewoof`; verify API (mc or curl)
|
||||||
|
|
||||||
|
## 2. Go Frontend
|
||||||
|
|
||||||
|
- [ ] 2.1 Add `github.com/minio/minio-go/v7`; config (`MINIO_ENDPOINT/ACCESS/SECRET/BUCKET`); client helper (no-op when unset)
|
||||||
|
- [ ] 2.2 `UploadPhoto` → PutObject (`tags/{id}.{ext}`) → set `photo_url=/photos/tags/{id}.{ext}`; new `GET /photos/{key...}` handler streaming GetObject with content-type + cache headers; route in main.go
|
||||||
|
- [ ] 2.3 Build + verify: upload via HTTP → object in MinIO → public page + `/photos/…` serve 200
|
||||||
|
|
||||||
|
## 3. Laravel Admin
|
||||||
|
|
||||||
|
- [ ] 3.1 `composer require league/flysystem-aws-s3-v3`; `config/filesystems.php` `minio` disk (endpoint .13:9000, use_ssl false); `admin/.env` MINIO creds
|
||||||
|
- [ ] 3.2 TagResource: photo `FileUpload` on the `minio` disk (path `tags/`), URL = `https://where-woof.com/photos/{path}`; verify upload from admin UI
|
||||||
|
|
||||||
|
## 4. Migration & Deploy
|
||||||
|
|
||||||
|
- [ ] 4.1 One-off copy `static/uploads/*` into MinIO (matching keys)
|
||||||
|
- [ ] 4.2 Deploy: frontend env (`MINIO_*` in ~/.config/where-woof.env) + rebuilt binary + restart; admin .env + rebuild container
|
||||||
|
- [ ] 4.3 Live round-trip: upload a photo via where-woof.com → loads; via admin → loads
|
||||||
|
- [ ] 4.4 `openspec validate photo-object-storage`; commit
|
||||||
Reference in New Issue
Block a user