Skip to content

SCRUM-217 — Config: content pack upload and list

Plan ref: CFG-B7 (docs/11-admin-plane-plan.md). Stacked on SCRUM-216.

Behaviour

POST /api/admin/config/packs?name=<name> (live_ops), body = raw .pck bytes: 1. clears this request's read and write deadlines (Config's own CONFIG_READ_TIMEOUT would otherwise kill a large upload that gateway_dev let through — the note from SCRUM-195); 2. name must match ^[a-z][a-z0-9_]{0,63}$ → else 400 validation_failed; 3. cap CONFIG_MAX_PACK_BYTES (default 512 MiB) → 413 body_too_large; 4. first 4 bytes must be GDPC (Godot pack) → else 400 "not a Godot content pack (missing GDPC header)", checked with bufio.Peek before anything is written; 5. streams into the blob store (temp file + sha256 in one pass + atomic rename) — never buffered; 6. INSERT … ON CONFLICT (sha256) DO NOTHING: new → 201 + audit pack.upload; identical bytes already stored (any name) → 200 with the original row, no new audit row, still one blob.

Response {pack_id, name, sha256, size, uploaded_by, uploaded_at}. GET /api/admin/config/packs (viewer) → {"packs":[…]} newest first. Metric config_pack_upload_bytes_total (both 201 and dedup 200 — the bytes crossed the wire either way).

How to verify

cd services/config
export CONFIG_TEST_DATABASE_URL='postgres://USER:PASS@127.0.0.1:5433/config_test?sslmode=disable'
go vet ./... && go test -race -count=1 ./...
go test -count=1 -v -run Pack ./internal/api/ ./internal/server/
Test Proves
TestCreatePackValidatesName name rules both ways
TestCreatePackRejectsNonGodotBodyWithoutWriting empty, G, NOPE, GDP, GDPX → 400 and nothing in the blob root or its tmp dir
TestCreatePackOverCapLeavesNothingBehind over the cap → 413, nothing left behind
TestCreatePackStoresThePeekedHeader the 4 peeked bytes are part of the stored blob
TestCreatePackStreamsWithoutBuffering 64 MiB generated body: TotalAlloc grows < 16 MiB; stored sha = generator's sha
TestPackUploadDeadlinesAreCleared server with 1s read/write timeouts, body stalls 2s → 201 (fails with 500 when the deadline-clearing call is removed — checked)
TestPackUploadBytesMetric counter adds the uploaded size
TestPacksRoutesThroughTheLiveServer 201 + one audit row; same bytes under another name → 200 with the original row, no new audit; list; viewer POST 403 / GET 200

By hand (live_ops token $T, through gateway_dev's packs route from SCRUM-195):

printf 'GDPC' > /tmp/p.pck; head -c 100M /dev/urandom >> /tmp/p.pck
curl -s -H "Authorization: Bearer $T" --data-binary @/tmp/p.pck "localhost:8090/api/admin/config/packs?name=season1_maps"

Results at time of writing

  • go vet, go test -race ./... against Postgres 16: pass (8 packages).

How it was built

DeepSeek run scoped (Landlock) to services/config (276 s, ~41k output tokens). Claude review: handler and dedup transaction read; mutation check on the deadline test. No changes needed.