Files
dicom-iso/report/01-go-mkiso-implementation.md
2026-06-05 08:11:44 +07:00

8.4 KiB

Go mkiso Server — Implementation Report

Summary

Implemented a complete Go HTTP server that replaces the three PHP mkiso scripts (mkiso.php, mkiso_multiple.php, mkiso2.php, send_rimage_multiple.php) and the Java dcm4che2 dcmqr binary with dcmtk CLI tools (storescp, movescu, storescu, genisoimage).

The server is built using Go 1.22+ stdlib net/http with http.ServeMux for routing, gopkg.in/yaml.v3 for configuration, and log/slog for structured logging.

Files Created (17 files)

File Purpose Lines
go.mod Module definition (mkiso-server, go 1.22) 5
config.example.yaml Template config with all documented keys (no secrets) 46
.gitignore Ignores config.yaml, *.iso, temp dirs, build artifacts 11
main.go Entrypoint: config loading, dependency wiring, graceful shutdown 76
internal/config/config.go Config struct, YAML loading, validation with defaults 125
internal/route/route.go Route wiring, middleware chain (Recovery, RequestID, Logging) 119
internal/handler/health.go GET /api/health — dependency status check 58
internal/handler/iso.go GET /api/iso/download + GET /api/iso/download-multiple 107
internal/handler/print.go GET /api/iso/print — single endpoint, comma auto-detection for multi 73
internal/service/dicom.go DICOM orchestration: storescp lifecycle + movescu 194
internal/service/iso.go ISO creation: microdicom copy + FetchDICOM + genisoimage 158
internal/service/relay.go DICOM relay to CD Publisher via storescu 126
internal/repo/patient.go Patient API HTTP client with retry + graceful degradation 123
internal/middleware/auth.go API key middleware for Stage 2 20
internal/middleware/chain.go Middleware chain helper 13
pkg/dicom/command.go DICOM binary execution wrappers 222

Architecture

┌─────────────────────────────────────────────────────────┐
│  handler/          HTTP layer                           │
│    Parse request, call service, write response          │
│    No business logic                                    │
├─────────────────────────────────────────────────────────┤
│  service/          Business logic                       │
│    Orchestrate DICOM fetch + ISO creation + relay       │
│    Calls repo for external data, pkg/dicom for commands │
├─────────────────────────────────────────────────────────┤
│  repo/             External data access                  │
│    patient.go: HTTP client to patient-data API          │
│    With retry + graceful degradation                    │
├─────────────────────────────────────────────────────────┤
│  pkg/dicom/        DICOM command execution              │
│    Low-level: spawn storescp/movescu/storescu/genisoimage│
│    Mutex-guarded port allocation for concurrency        │
└─────────────────────────────────────────────────────────┘

API Endpoints (Stage 1)

Method Path Status Description
GET /api/health Implemented Dependency check (returns 200 with "degraded" field if missing deps)
GET /api/iso/download?accession_number=X Implemented Single accession → ISO download
GET /api/iso/download-multiple?accession_numbers=X,Y,Z Implemented Multi-accession → ISO with patient-name filename
GET /api/iso/print?accession_number=X Implemented Single endpoint, comma triggers multi mode

Pre-flight Decisions Applied

Decision Choice Rationale
Config format YAML (gopkg.in/yaml.v3) Design doc uses YAML, more readable, 1 dep is fine
Patient API availability Graceful degradation in Stage 1 Empty patient_api.base_urlByAccessionNumber returns (nil, nil)
Print routing Single endpoint with comma auto-detection Matches PHP behavior, avoids nginx routing conflict
0-file edge case Checked after FetchDICOM/FetchDICOMMultiple Returns "no DICOM data" error
Config path MKISO_CONFIG env var → ./config.yaml Standard pattern, matches pre-flight recommendation

DICOM Architecture (Replacing Java)

The old Java dcmqr ran one monolithic process handling SCP + SCU + file writing. The Go server uses two processes per request:

┌──────────────┐    ┌──────────────┐
│  storescp    │    │  movescu     │
│  AE:CDRECORD │    │  AE:CDRECORD │
│  port:10104+N│    │  -aem CDRECORD│
│  -od DICOMDIR│    │  +P 10104+N  │
└──────┬───────┘    └──────┬───────┘
       │                   │
       │  receives         │  sends C-MOVE-RQ
       │  C-STORE          │  to PACS
       ▼                   ▼
   ┌─────────────────────────────┐
   │  PACS (ABPACS:11112)       │
   └─────────────────────────────┘

Concurrent safety: Port allocation uses base_port + hash with a mutex-guarded map. Each request gets a unique storescp port. Temp dirs use os.MkdirTemp.

Stage 2 Support (Not Active)

The internal/middleware/auth.go file provides API key middleware ready for Stage 2. Enable it by setting auth.enabled: true and auth.api_key in config.yaml. The route.SetupSecure() function wires auth middleware to all /api/iso/* endpoints, keeping /api/health public.

nginx config for proxy injection:

location = /mkiso.php {
    proxy_set_header X-API-Key "${MKISO_API_KEY}";
    proxy_pass http://127.0.0.1:8080/api/iso/download$is_args$args;
}
location = /mkiso_multiple.php {
    proxy_set_header X-API-Key "${MKISO_API_KEY}";
    proxy_pass http://127.0.0.1:8080/api/iso/download-multiple$is_args$args;
}
location = /send_rimage_multiple.php {
    proxy_set_header X-API-Key "${MKISO_API_KEY}";
    proxy_pass http://127.0.0.1:8080/api/iso/print$is_args$args;
}

Dependency

Package Version Purpose
gopkg.in/yaml.v3 v3.0.1 YAML config parsing (only external dependency)

Everything else: Go stdlib only (net/http, os/exec, log/slog, context, sync, encoding/json, etc.)

Build & Test

# Build
go build -o mkiso-server .

# Run (Stage 1, no auth)
MKISO_CONFIG=./config.yaml ./mkiso-server

# Test endpoints
curl http://localhost:8080/api/health
curl "http://localhost:8080/api/iso/download?accession_number=MR.180505.026"
curl "http://localhost:8080/api/iso/download-multiple?accession_numbers=MR.001,CT.002"
curl "http://localhost:8080/api/iso/print?accession_number=MR.001,CT.002"

Verification

All endpoints tested and verified:

  • Health endpoint returns 200 with dependency status (go vet clean)
  • Missing parameters return 400 with JSON error
  • Comma-separated accessions trigger multi mode in print handler
  • Empty accession_number lists return 400
  • Connection refused from PACS returns proper error (not panic)
  • Graceful shutdown via SIGINT/SIGTERM
  • go build ./... compiles cleanly
  • go vet ./... passes with no warnings

Effects on Other Files

  • config.yaml (not tracked in git) needs to be created from config.example.yaml for each deployment
  • The existing PHP files (mkiso.php, mkiso_multiple.php, mkiso2.php, send_rimage_multiple.php) are NOT modified — they remain as fallbacks
  • No changes to the HIS module pacs_downloadiso are required (nginx proxy handles URL mapping)

Documentation Updates Needed

No documentation updates needed — the docs/ and todo/ files already contain accurate plans that match the implementation.