feat: base go mkiso
This commit is contained in:
190
todo/go-mkiso-implementation.md
Normal file
190
todo/go-mkiso-implementation.md
Normal file
@@ -0,0 +1,190 @@
|
||||
# Go mkiso Server — Implementation Checklist
|
||||
|
||||
## Stage 1 — Plain MVP (no auth)
|
||||
|
||||
Goal: Functional replacement for all three PHP scripts. No security — plain GET endpoints.
|
||||
|
||||
- [ ] 1. **Project scaffolding**
|
||||
- Create `go.mod` (`module mkiso-server`, go 1.22+)
|
||||
- Create directory structure (`internal/{config,route,handler,service,repo}`, `pkg/dicom`)
|
||||
- Create `config.example.yaml` with all documented keys (no secrets)
|
||||
- Create `.gitignore` (ignore `config.yaml`, `*.iso`, temp dirs)
|
||||
|
||||
- [ ] 2. **Config loading** — `internal/config/config.go`
|
||||
- Define `Config` struct matching `docs/go-mkiso-design.md` § config.yaml
|
||||
- Load from YAML (`gopkg.in/yaml.v3`) or JSON (`encoding/json` for zero deps)
|
||||
- Validate required fields on load (pacs host, dcmtk paths)
|
||||
- ⚠️ Skip `auth:` section for Stage 1
|
||||
|
||||
- [ ] 3. **DICOM command runner** — `pkg/dicom/command.go`
|
||||
- `StartStoresCP`, `StopStoresCP`, `RunMoveSCU`, `RunStoreSCU`, `RunGenISOImage`
|
||||
- Port allocation: `allocatePort(basePort, portRange)` — mutex-guarded map
|
||||
- All functions return `(exitCode int, stdout, stderr string, error)`
|
||||
- Logging with slog
|
||||
|
||||
- [ ] 4. **Patient repo** — `internal/repo/patient.go`
|
||||
- HTTP client to patient-data API (see `docs/patient-api-spec.md`)
|
||||
- `ByAccessionNumber(ctx, accessionNumber) → (*PatientData, error)`
|
||||
- Configurable auth header from config (for when patient API itself needs auth)
|
||||
- Timeout + retry with backoff
|
||||
|
||||
- [ ] 5. **DICOM service** — `internal/service/dicom.go`
|
||||
- `FetchDICOM(ctx, accessionNumber, destDir) → (filesCount int, error)`
|
||||
- `FetchDICOMMultiple(ctx, accessionNumbers []string, destDir) → (totalFiles int, error)`
|
||||
- storescp lifecycle: start → wait ready → movescu → stop
|
||||
- `defer` for guaranteed storescp cleanup
|
||||
- ⚠️ After FetchDICOM, check `filesCount > 0`. If 0, return error `"no DICOM data"` before attempting ISO/genisoimage.
|
||||
|
||||
- [ ] 6. **ISO service** — `internal/service/iso.go`
|
||||
- `GenerateISO(ctx, accessionNumber) → (isoPath string, cleanup func(), error)`
|
||||
- `GenerateISOMultiple(ctx, accessionNumbers) → (isoPath string, cleanup func(), error)`
|
||||
- Temp dir + microdicom copy + FetchDICOM + genisoimage
|
||||
- Patient API for multi-accession filename
|
||||
|
||||
- [ ] 7. **Relay service** — `internal/service/relay.go` (see `todo/go-print-relay.detail.md`)
|
||||
- `RelayToCDPublisher(ctx, accessionNumbers) → (*RelayResult, error)`
|
||||
- Patient API validation + FetchDICOM + storescu to CD Publisher
|
||||
- Response includes `patient_name`, `destination`, `files_sent`
|
||||
|
||||
- [ ] 8. **Handlers** — `internal/handler/{health,iso,print}.go`
|
||||
- `GET /api/health` — health check + dependency status
|
||||
- `GET /api/iso/download?accession_number=X` — single ISO download
|
||||
- `GET /api/iso/download-multiple?accession_numbers=X,Y,Z` — multi ISO download
|
||||
- `GET /api/iso/print?accession_number=X` — DICOM relay (auto-detects comma → multi)
|
||||
- ⚠️ Print uses ONE endpoint for both single and multi: comma in `accession_number` triggers multi mode, no `print-multiple` endpoint needed (see pre-flight gap #3)
|
||||
|
||||
- [ ] 9. **Route wiring** — `internal/route/route.go`
|
||||
- Go 1.22+ `http.NewServeMux` with method+path patterns
|
||||
- Request ID middleware (UUID, `X-Request-ID` header)
|
||||
- Recovery middleware (catch panics, log stack, return 500)
|
||||
- **Stage 1**: 4 endpoints — health, download, download-multiple, print (single endpoint, auto-detects commas)
|
||||
- **No auth middleware in Stage 1**
|
||||
|
||||
- [ ] 10. **Main entrypoint** — `main.go`
|
||||
- Load config, wire dependencies, start server
|
||||
- Graceful shutdown (SIGINT/SIGTERM)
|
||||
- `slog.Info("server started", "port", cfg.Server.Port)`
|
||||
|
||||
- [ ] 11. **Stage 1 testing**
|
||||
- Test all four endpoints with curl (no auth header needed)
|
||||
- Test concurrent requests
|
||||
- Test error cases
|
||||
- Test through nginx reverse proxy
|
||||
|
||||
---
|
||||
|
||||
## Stage 2 — Add Authentication
|
||||
|
||||
Goal: Lock down endpoints. **Suggestion below**, implementor picks approach.
|
||||
|
||||
### Recommended: API Key via Reverse Proxy Injection
|
||||
|
||||
**Why**: Zero changes to the HIS `pacs_downloadiso` module. The existing `window.open()` calls continue to work because nginx injects the auth header before proxying to Go.
|
||||
|
||||
```
|
||||
Browser (HIS) nginx (PACS server) Go Server
|
||||
──────────── ─────────────────── ─────────
|
||||
window.open("/mkiso.php?..")
|
||||
──────────── GET ───────────→ add X-API-Key header
|
||||
───── GET + key ──────→ validate key → 200 OK
|
||||
←─── ISO stream ───────
|
||||
←── ISO download ────────────
|
||||
```
|
||||
|
||||
**nginx config:**
|
||||
```nginx
|
||||
location ~ ^/(mkiso|mkiso_multiple|send_rimage_multiple)\.php$ {
|
||||
proxy_set_header X-API-Key "${MKISO_API_KEY}";
|
||||
proxy_pass http://127.0.0.1:8080/api/iso/$1$is_args$args;
|
||||
}
|
||||
|
||||
# Direct API access (for testing/other clients)
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
}
|
||||
```
|
||||
|
||||
**Go middleware (simple):**
|
||||
```go
|
||||
func APIKey(expectedKey string) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("X-API-Key") != expectedKey {
|
||||
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Zero HIS code changes — nginx handles it
|
||||
- Simple to implement (one string comparison in middleware)
|
||||
- No token expiry, no login endpoint needed
|
||||
- Key rotation = update config.yaml + reload nginx
|
||||
|
||||
**Cons:**
|
||||
- Single shared key (no user-level audit)
|
||||
- Key visible in nginx config
|
||||
- No token expiry (must rotate manually)
|
||||
|
||||
### Alternative Suggestions
|
||||
|
||||
| Approach | Complexity | HIS Changes | Best For |
|
||||
|----------|-----------|-------------|----------|
|
||||
| **API Key (recommended)** | Low | None | Internal hospital network, nginx proxy in place |
|
||||
| **Hardcoded JWT** | Low | None (nginx injects) | When you want expiry + claims but no login flow |
|
||||
| **JWT + Login endpoint** | Medium | Yes (or client script) | Multi-client, need user-level audit |
|
||||
| **IP whitelist** | Low | None | Fixed HIS server IP, simplest possible |
|
||||
| **mTLS** | High | Yes (client cert) | Strictest security, external-facing |
|
||||
|
||||
### Stage 2 Todo Items
|
||||
|
||||
- [ ] 12. **Config additions** — `internal/config/config.go`
|
||||
- Add `auth.api_key` field (the expected key)
|
||||
- Add `auth.enabled` boolean (false in Stage 1 config, true in Stage 2)
|
||||
|
||||
- [ ] 13. **API Key middleware** — `internal/middleware/auth.go`
|
||||
- Extract `X-API-Key` from request header
|
||||
- Compare against config value
|
||||
- Return 401 on mismatch
|
||||
- Skip health endpoint (`/api/health` stays public)
|
||||
|
||||
- [ ] 14. **Route wiring update** — `internal/route/route.go`
|
||||
- Apply API key middleware to all `/api/iso/*` routes (download, download-multiple, print)
|
||||
- Keep `/api/health` public
|
||||
|
||||
- [ ] 15. **nginx config** — deploy reverse proxy with header injection
|
||||
- Map old PHP paths to new Go API paths
|
||||
- Inject `X-API-Key` header
|
||||
- Verify HIS `window.open()` calls work end-to-end
|
||||
|
||||
- [ ] 16. **Stage 2 testing**
|
||||
- Test with valid API key → 200
|
||||
- Test with invalid API key → 401
|
||||
- Test with missing API key → 401
|
||||
- Test health endpoint still public
|
||||
- Test through nginx proxy (key injected) → 200
|
||||
- Test direct access without nginx → 401
|
||||
|
||||
---
|
||||
|
||||
## Stage 3 (Future)
|
||||
|
||||
- [ ] JWT login endpoint (if multi-user audit needed)
|
||||
- [ ] Rate limiting
|
||||
- [ ] Metrics / Prometheus endpoint
|
||||
- [ ] Docker image
|
||||
- [ ] Remove Java/PHP after Go verified in production
|
||||
|
||||
---
|
||||
|
||||
## Dependencies (go.mod)
|
||||
|
||||
| Package | Purpose |
|
||||
|---------|---------|
|
||||
| `gopkg.in/yaml.v3` | YAML config parsing *(only external dep)* |
|
||||
|
||||
Everything else: **Go stdlib only**.
|
||||
Reference in New Issue
Block a user