feat: base go mkiso
This commit is contained in:
430
docs/go-mkiso-design.md
Normal file
430
docs/go-mkiso-design.md
Normal file
@@ -0,0 +1,430 @@
|
||||
# Go mkiso Server — Architecture & Design
|
||||
|
||||
## Overview
|
||||
|
||||
A Go HTTP server (stdlib only, no frameworks) that replaces the PHP mkiso scripts. It handles DICOM retrieval from PACS (via dcmtk CLI), ISO creation (via genisoimage), and streaming download — all behind JWT authentication.
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
mkiso-server/
|
||||
├── config.yaml # Configuration (PACS, AE, binaries, JWT)
|
||||
├── config.example.yaml # Template config without secrets
|
||||
├── go.mod
|
||||
├── main.go # Entrypoint: load config, wire deps, start server
|
||||
├── internal/
|
||||
│ ├── config/
|
||||
│ │ └── config.go # Config struct, YAML loading
|
||||
│ ├── route/
|
||||
│ │ └── route.go # Route registration, middleware chain
|
||||
│ ├── middleware/
|
||||
│ │ └── auth.go # JWT middleware (slog, context injection)
|
||||
│ ├── handler/
|
||||
│ │ ├── auth.go # POST /api/auth/login
|
||||
│ │ ├── iso.go # ISO download handlers
|
||||
│ │ ├── print.go # Print ISO (stub)
|
||||
│ │ └── health.go # GET /api/health
|
||||
│ ├── service/
|
||||
│ │ ├── dicom.go # DICOM operations (storescp + movescu orchestration)
|
||||
│ │ ├── iso.go # ISO creation + cleanup
|
||||
│ │ └── auth.go # JWT generation + validation
|
||||
│ └── repo/
|
||||
│ └── patient.go # HTTP client to patient-data API
|
||||
├── pkg/
|
||||
│ └── dicom/
|
||||
│ └── command.go # Low-level dcmtk binary execution wrapper
|
||||
└── testdata/
|
||||
└── ... # Test fixtures
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## External Dependencies
|
||||
|
||||
| Binary | Package | Config Key |
|
||||
|--------|---------|------------|
|
||||
| `storescp` | dcmtk | `dcmtk.storescp` |
|
||||
| `movescu` | dcmtk | `dcmtk.movescu` |
|
||||
| `genisoimage` | `genisoimage` (apt) | `tools.genisoimage` |
|
||||
|
||||
**All binary paths are in `config.yaml`.** No hardcoded paths.
|
||||
|
||||
---
|
||||
|
||||
## config.yaml Design
|
||||
|
||||
```yaml
|
||||
server:
|
||||
port: 8080
|
||||
read_timeout: 300s # ISO generation can take minutes
|
||||
write_timeout: 600s
|
||||
|
||||
auth:
|
||||
enabled: false # Stage 1: false (no auth). Stage 2: true
|
||||
api_key: "" # Stage 2: shared key, checked via X-API-Key header
|
||||
|
||||
dcmtk:
|
||||
storescp: "/usr/local/bin/storescp"
|
||||
movescu: "/usr/local/bin/movescu"
|
||||
storescu: "/usr/local/bin/storescu"
|
||||
|
||||
tools:
|
||||
genisoimage: "/usr/bin/genisoimage"
|
||||
|
||||
pacs:
|
||||
ae_title: "ABPACS"
|
||||
host: "localhost"
|
||||
port: 11112
|
||||
|
||||
our_ae:
|
||||
ae_title: "CDRECORD"
|
||||
base_port: 10104 # storescp listen port range start
|
||||
port_range: 100 # 10104-10203 (100 unique ports for concurrent requests)
|
||||
|
||||
patient_api:
|
||||
base_url: "http://his-server/api" # External patient data API
|
||||
timeout: 10s
|
||||
|
||||
cd_publisher:
|
||||
host: "172.16.0.120" # CD Publisher server (for print/relay)
|
||||
port: 104 # DICOM port on CD Publisher
|
||||
|
||||
iso:
|
||||
microdicom_path: "/var/www/html/microdicom" # DICOM viewer bootstrap files
|
||||
temp_dir: "/tmp" # Working directory for dicomdir_*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Stage 1 — All public (auth.enabled = false)
|
||||
|
||||
| Method | Path | Description | Replaces |
|
||||
|--------|------|-------------|----------|
|
||||
| `GET` | `/api/health` | Health check + dependency status | — |
|
||||
| `GET` | `/api/iso/download?accession_number=X` | Single accession → ISO download | `mkiso.php` |
|
||||
| `GET` | `/api/iso/download-multiple?accession_numbers=X,Y,Z` | Multi-accession → ISO download with patient-name filename | `mkiso_multiple.php` |
|
||||
| `GET` | `/api/iso/print?accession_number=X` | DICOM relay → CD Publisher (auto-detects comma → multi) | `send_rimage_multiple.php` |
|
||||
| `GET` | `/api/iso/print?accession_number=X,Y,Z` | (same endpoint — comma triggers multi) | `send_rimage_multiple.php` |
|
||||
|
||||
> **Note:** `print` and `print-multiple` are merged into one endpoint. The handler checks for commas in `accession_number` to decide single vs multi mode. This matches the PHP behavior where `send_rimage_multiple.php` handles both via `explode(",", ...)`.
|
||||
|
||||
### Stage 2 — Authenticated (auth.enabled = true)
|
||||
|
||||
Same endpoints, but `/api/iso/*` requires `X-API-Key` header. `/api/health` stays public.
|
||||
|
||||
```
|
||||
GET /api/iso/download?accession_number=X
|
||||
X-API-Key: <shared-key-from-config>
|
||||
|
||||
GET /api/iso/print?accession_number=X,Y,Z
|
||||
X-API-Key: <shared-key-from-config>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Authentication (Stage 2)
|
||||
|
||||
### Recommended: API Key via nginx 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 ──
|
||||
```
|
||||
|
||||
**Config:**
|
||||
```yaml
|
||||
auth:
|
||||
enabled: true # Stage 1: false, Stage 2: true
|
||||
api_key: "changeme" # Shared key — keep secret
|
||||
```
|
||||
|
||||
**Middleware (~10 lines, stdlib only):**
|
||||
```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 {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte(`{"error":"unauthorized"}`))
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**nginx reverse proxy config:**
|
||||
```nginx
|
||||
# Map old PHP paths to Go API + inject auth
|
||||
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;
|
||||
}
|
||||
```
|
||||
|
||||
**Route wiring (Stage 2):**
|
||||
```go
|
||||
mux.HandleFunc("GET /api/health", handler.Health(cfg)) // always public
|
||||
|
||||
apiKey := middleware.APIKey(cfg.Auth.APIKey)
|
||||
mux.Handle("GET /api/iso/download", apiKey(handler.DownloadSingle(isoSvc)))
|
||||
mux.Handle("GET /api/iso/download-multiple", apiKey(handler.DownloadMultiple(isoSvc)))
|
||||
// ... same for print endpoints
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DICOM Flow (Go — replaces Java dcmqr)
|
||||
|
||||
### Single Accession (`/api/iso/download`)
|
||||
|
||||
```
|
||||
1. handler.iso.go: DownloadSingle()
|
||||
├─ Parse accession_number from query
|
||||
├─ Create temp dir: /tmp/dicomdir_<uuid>/
|
||||
├─ service.iso.go: GenerateISO()
|
||||
│ ├─ service.dicom.go: FetchDICOM()
|
||||
│ │ ├─ pkg/dicom: StartStoresCP() → storescp as child process
|
||||
│ │ │ storescp <port> -aet CDRECORD -od <dir> +xa
|
||||
│ │ ├─ Wait for storescp ready (poll / retry)
|
||||
│ │ ├─ pkg/dicom: RunMoveSCU() → movescu as child process
|
||||
│ │ │ movescu -aet CDRECORD -aec ABPACS -aem CDRECORD
|
||||
│ │ │ localhost 11112 -S -k 0008,0050=<acc>
|
||||
│ │ │ -k 0010,0020= --no-port
|
||||
│ │ ├─ Wait for movescu (with timeout)
|
||||
│ │ └─ pkg/dicom: StopStoresCP() → SIGTERM
|
||||
│ ├─ Copy microdicom viewer files into temp dir
|
||||
│ ├─ Run genisoimage
|
||||
│ │ genisoimage -iso-level 4 -r -V DICOM -o <acc>.iso <dir>
|
||||
│ └─ Return ISO path
|
||||
├─ Stream ISO to response (Content-Type: application/octet-stream)
|
||||
├─ Cleanup temp dir
|
||||
└─ Return
|
||||
```
|
||||
|
||||
### Multiple Accessions (`/api/iso/download-multiple`)
|
||||
|
||||
Same as single but:
|
||||
1. Start `storescp` ONCE for all accessions
|
||||
2. Run `movescu` in a loop for each accession number
|
||||
3. Call **patient API** to get `Nama` for filename
|
||||
4. ISO filename: `{PATIENT_NAME}-{ACC_LIST}.iso`
|
||||
5. Cleanup after everything
|
||||
|
||||
### Print / DICOM Relay (`/api/iso/print`, `/api/iso/print-multiple`)
|
||||
|
||||
Replaces `send_rimage_multiple.php`. Same DICOM fetch as download, then C-STORE relay instead of ISO. Uses patient API for validation (same as download-multiple).
|
||||
|
||||
```
|
||||
1. handler.print.go: PrintISO()
|
||||
├─ Parse accession_number(s) from query
|
||||
├─ Call patient API → validate accession, get patient_name
|
||||
├─ Create temp dir: /tmp/dicomdir_<uuid>/
|
||||
├─ service.relay.go: RelayToCDPublisher()
|
||||
│ ├─ Copy microdicom viewer files into temp dir
|
||||
│ ├─ service.dicom.go: FetchDICOM() → storescp + movescu loop
|
||||
│ ├─ pkg/dicom: RunStoreSCU() → storescu to CD Publisher
|
||||
│ │ storescu -aet <our_ae> +sd +r <cd_host> <cd_port> <dir>
|
||||
│ ├─ Count files sent
|
||||
│ └─ Return relay result with patient_name
|
||||
├─ Respond JSON: { status, accessions_sent, patient_name, destination, files_sent }
|
||||
├─ Cleanup temp dir
|
||||
└─ Return
|
||||
```
|
||||
|
||||
**Key difference from download:** No genisoimage, no streaming. Instead `storescu +sd +r` (scan directory + recurse) sends all DICOM files to the CD Publisher server. Response is JSON (success/failure), not an ISO file. Patient API is used for accession validation, not just filename.
|
||||
|
||||
### Concurrent Safety
|
||||
|
||||
```
|
||||
Port allocation: base_port + hash(request_id) % port_range
|
||||
Temp dir: /tmp/dicomdir_<uuid>/
|
||||
```
|
||||
|
||||
Multiple incoming requests each get their own `storescp` port and temp directory. No collisions.
|
||||
|
||||
---
|
||||
|
||||
## Patient Data API Specification
|
||||
|
||||
The Go server does NOT access the database directly. It calls an external API for patient data. This is defined in `docs/patient-api-spec.md`.
|
||||
|
||||
### Endpoint: `GET /patient/by-accession`
|
||||
|
||||
```
|
||||
Request:
|
||||
GET {patient_api.base_url}/patient/by-accession?accession_number=MR.2024.001
|
||||
Authorization: Bearer <service-token> (or API key header)
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"accession_number": "MR.2024.001",
|
||||
"medrec_id": "00012345",
|
||||
"reg_id": "REG-2024-00123",
|
||||
"patient_name": "JOHN DOE",
|
||||
"modality": "MR",
|
||||
"study_description": "MRI Brain"
|
||||
}
|
||||
|
||||
Response 404:
|
||||
{ "error": "accession_number not found" }
|
||||
```
|
||||
|
||||
This API must be provided by the HIS team (or deploy a thin proxy). The Go server calls it with HTTP GET.
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Scenario | HTTP Status | Response |
|
||||
|----------|-------------|----------|
|
||||
| Invalid/missing accession_number | `400` | `{"error": "invalid accession_number"}` |
|
||||
| PACS unreachable | `502` | `{"error": "PACS unreachable", "detail": "..."}` |
|
||||
| movescu failed (accession not found) | `404` | `{"error": "no DICOM data for accession_number"}` |
|
||||
| Patient API unavailable | `502` | `{"error": "patient API unavailable"}` |
|
||||
| genisoimage failed | `500` | `{"error": "ISO creation failed"}` |
|
||||
| Print ISO — CD Publisher unreachable | `502` | `{"error": "CD Publisher unreachable", "destination": "..."}` |
|
||||
| Concurrent port exhaustion | `503` | `{"error": "too many concurrent requests"}` |
|
||||
| Timeout (C-MOVE) | `504` | `{"error": "PACS move timed out"}` |
|
||||
|
||||
**Stage 2 additions:**
|
||||
|
||||
| Scenario | HTTP Status | Response |
|
||||
|----------|-------------|----------|
|
||||
| Missing/invalid API key | `401` | `{"error": "unauthorized"}` |
|
||||
|
||||
---
|
||||
|
||||
## Logging (slog)
|
||||
|
||||
All components use `log/slog`. Key log points:
|
||||
|
||||
```go
|
||||
slog.Info("starting storescp", "port", port, "dir", dir, "pid", pid)
|
||||
slog.Info("running movescu", "accession", acc, "pacs", pacs)
|
||||
slog.Info("iso created", "path", isoPath, "size_bytes", size, "duration_ms", d)
|
||||
slog.Error("movescu failed", "accession", acc, "exit_code", code, "stderr", err)
|
||||
slog.Warn("patient API slow", "latency_ms", lat)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Route Wiring (stdlib net/http)
|
||||
|
||||
```go
|
||||
// internal/route/route.go
|
||||
|
||||
// Stage 1 — no auth
|
||||
func Setup(cfg *config.Config, isoSvc service.ISOService, relaySvc service.RelayService) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("GET /api/health", handler.Health(cfg))
|
||||
mux.HandleFunc("GET /api/iso/download", handler.DownloadSingle(isoSvc))
|
||||
mux.HandleFunc("GET /api/iso/download-multiple", handler.DownloadMultiple(isoSvc))
|
||||
mux.HandleFunc("GET /api/iso/print", handler.PrintISO(relaySvc)) // auto-detects single vs multi
|
||||
|
||||
return middleware.Recovery(middleware.RequestID(mux))
|
||||
}
|
||||
|
||||
// Stage 2 — API key auth on /api/iso/*
|
||||
func SetupSecure(cfg *config.Config, isoSvc service.ISOService, relaySvc service.RelayService) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("GET /api/health", handler.Health(cfg)) // always public
|
||||
|
||||
apiKey := middleware.APIKey(cfg.Auth.APIKey)
|
||||
mux.Handle("GET /api/iso/download", apiKey(http.HandlerFunc(handler.DownloadSingle(isoSvc))))
|
||||
mux.Handle("GET /api/iso/download-multiple", apiKey(http.HandlerFunc(handler.DownloadMultiple(isoSvc))))
|
||||
mux.Handle("GET /api/iso/print", apiKey(http.HandlerFunc(handler.PrintISO(relaySvc))))
|
||||
|
||||
return middleware.Recovery(middleware.RequestID(mux))
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Repository Pattern Boundaries
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ handler/ HTTP layer │
|
||||
│ Parse request, call service, write response │
|
||||
│ No business logic │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ service/ Business logic │
|
||||
│ Orchestrate DICOM fetch + ISO creation + cleanup │
|
||||
│ Calls repo for external data, pkg/dicom for commands │
|
||||
│ Uses slog for logging │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ repo/ External data access │
|
||||
│ patient.go: HTTP client to patient-data API │
|
||||
│ Uses net/http with timeout + retry │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ pkg/dicom/ DICOM command execution │
|
||||
│ Low-level: spawn storescp/movescu, monitor, kill │
|
||||
│ Returns (filesRetrieved int, error) │
|
||||
│ Pure os/exec, no business logic │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Replacement Mapping
|
||||
|
||||
| Old (PHP on PACS server) | New (Go on PACS server) |
|
||||
|--------------------------|-------------------------|
|
||||
| `mkiso.php?accession_number=X` | `GET /api/iso/download?accession_number=X` |
|
||||
| `mkiso_multiple.php?accession_number=X,Y,Z` | `GET /api/iso/download-multiple?accession_numbers=X,Y,Z` |
|
||||
| `send_rimage_multiple.php?accession_number=X` | `GET /api/iso/print?accession_number=X` |
|
||||
| `send_rimage_multiple.php?accession_number=X,Y,Z` | `GET /api/iso/print?accession_number=X,Y,Z` (same endpoint, comma auto-detect) |
|
||||
| Direct DB: `pacsdb_his` → `pacs_result_series`, `medrec` | HTTP: patient API |
|
||||
|
||||
### HIS Module Impact
|
||||
|
||||
The `pacs_downloadiso` module JS opens `window.open("http://{PACS_HOST}/mkiso.php?accession_number=X")`.
|
||||
|
||||
**Recommended: nginx reverse proxy** — zero HIS code changes, works in both stages:
|
||||
|
||||
```nginx
|
||||
# Map old PHP paths to new Go API paths
|
||||
|
||||
# mkiso.php → /api/iso/download
|
||||
location = /mkiso.php {
|
||||
proxy_set_header X-API-Key "${MKISO_API_KEY}"; # Stage 2 only
|
||||
proxy_pass http://127.0.0.1:8080/api/iso/download$is_args$args;
|
||||
}
|
||||
|
||||
# mkiso_multiple.php → /api/iso/download-multiple
|
||||
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;
|
||||
}
|
||||
|
||||
# send_rimage_multiple.php → /api/iso/print
|
||||
# (single handler auto-detects commas for multi mode)
|
||||
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;
|
||||
}
|
||||
```
|
||||
180
docs/howto-manual-iso.md
Normal file
180
docs/howto-manual-iso.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# Manual ISO Creation — HOWTO
|
||||
|
||||
Create a bootable DICOM viewer ISO with patient images, using `genisoimage` (or `mkisofs` / `xorriso`).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
| Tool | Check | Install (Arch) |
|
||||
|------|-------|----------------|
|
||||
| `genisoimage` | `which genisoimage` | `pacman -S cdrtools` + symlink, or `pacman -S libisoburn` (xorriso) |
|
||||
| Raw viewer files | `raw/microdicom/` | Part of this project |
|
||||
| DICOM files | From PACS or unzipped archive | e.g. `unzip pasien.zip` |
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Prepare working directory
|
||||
|
||||
```bash
|
||||
mkdir -p /tmp/build_iso/xcdrom/DICOMDIR
|
||||
```
|
||||
|
||||
Directory structure yang akan jadi root ISO:
|
||||
|
||||
```
|
||||
/tmp/build_iso/xcdrom/ ← root ISO
|
||||
├── AUTORUN.INF
|
||||
├── INDEX.PHP
|
||||
├── README.TXT
|
||||
├── RUN.BAT
|
||||
├── MICROD/ ← DICOM viewer
|
||||
└── DICOMDIR/ ← file DICOM pasien
|
||||
```
|
||||
|
||||
### 2. Copy microdicom viewer
|
||||
|
||||
```bash
|
||||
cp -r /path/to/raw/microdicom/* /tmp/build_iso/xcdrom/
|
||||
```
|
||||
|
||||
Hasil:
|
||||
```
|
||||
/tmp/build_iso/xcdrom/AUTORUN.INF
|
||||
/tmp/build_iso/xcdrom/INDEX.PHP
|
||||
/tmp/build_iso/xcdrom/README.TXT
|
||||
/tmp/build_iso/xcdrom/RUN.BAT
|
||||
/tmp/build_iso/xcdrom/MICROD/
|
||||
```
|
||||
|
||||
### 3. Copy DICOM files pasien
|
||||
|
||||
**Flat** (semua file langsung di DICOMDIR):
|
||||
|
||||
```bash
|
||||
cp /path/to/dicom/files/* /tmp/build_iso/xcdrom/DICOMDIR/
|
||||
```
|
||||
|
||||
**Dari unzip dengan subfolder** (misal hasil unzip `EF76D893/...`):
|
||||
|
||||
```bash
|
||||
find EF76D893 -type f -exec cp {} /tmp/build_iso/xcdrom/DICOMDIR/ \;
|
||||
```
|
||||
|
||||
**Verifikasi**:
|
||||
|
||||
```bash
|
||||
find /tmp/build_iso/xcdrom -type f | sort
|
||||
```
|
||||
|
||||
### 4. Build ISO
|
||||
|
||||
```bash
|
||||
genisoimage \
|
||||
-iso-level 4 \
|
||||
-r \
|
||||
-allow-multidot \
|
||||
-allow-lowercase \
|
||||
-allow-leading-dots \
|
||||
-V DICOM \
|
||||
-o /tmp/output.iso \
|
||||
/tmp/build_iso/xcdrom
|
||||
```
|
||||
|
||||
| Flag | Fungsi |
|
||||
|------|--------|
|
||||
| `-iso-level 4` | Mengizinkan direktori >8 level |
|
||||
| `-r` | Rock Ridge (long filename, Unix permissions) |
|
||||
| `-allow-multidot` | Mengizinkan multiple dots dalam filename |
|
||||
| `-allow-lowercase` | Lowercase filename |
|
||||
| `-allow-leading-dots` | File dimulai dengan titik (.) |
|
||||
| `-V DICOM` | Volume label = `DICOM` |
|
||||
| `-o output.iso` | File ISO tujuan |
|
||||
|
||||
### 5. Verify ISO
|
||||
|
||||
```bash
|
||||
# Cek ukuran
|
||||
ls -lh /tmp/output.iso
|
||||
|
||||
# Cek struktur isi
|
||||
isoinfo -l -i /tmp/output.iso
|
||||
|
||||
# Cek format
|
||||
file /tmp/output.iso
|
||||
# Output: ISO 9660 CD-ROM filesystem data 'DICOM'
|
||||
|
||||
# Mount & inspect (optional)
|
||||
mkdir -p /tmp/mnt_iso
|
||||
sudo mount -o loop /tmp/output.iso /tmp/mnt_iso
|
||||
find /tmp/mnt_iso -type f -ls
|
||||
sudo umount /tmp/mnt_iso
|
||||
rmdir /tmp/mnt_iso
|
||||
```
|
||||
|
||||
## Shortcut (one-liner)
|
||||
|
||||
Gabung semua step dalam satu script:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
ISO_NAME="${1:-output}"
|
||||
SRC_DIR="/tmp/build_iso/xcdrom"
|
||||
ISO_FILE="/tmp/${ISO_NAME}.iso"
|
||||
|
||||
mkdir -p "$SRC_DIR/DICOMDIR"
|
||||
cp -r /path/to/raw/microdicom/* "$SRC_DIR/"
|
||||
cp /path/to/dicom/files/* "$SRC_DIR/DICOMDIR/"
|
||||
|
||||
genisoimage \
|
||||
-iso-level 4 -r \
|
||||
-allow-multidot -allow-lowercase -allow-leading-dots \
|
||||
-V DICOM \
|
||||
-o "$ISO_FILE" \
|
||||
"$SRC_DIR"
|
||||
|
||||
echo "ISO created: $ISO_FILE ($(du -h "$ISO_FILE" | cut -f1))"
|
||||
```
|
||||
|
||||
## Alternative: xorriso
|
||||
|
||||
If you have `xorriso` instead of `genisoimage`:
|
||||
|
||||
```bash
|
||||
xorriso -as mkisofs \
|
||||
-iso-level 4 \
|
||||
-r \
|
||||
-V DICOM \
|
||||
-o /tmp/output.iso \
|
||||
/tmp/build_iso/xcdrom
|
||||
```
|
||||
|
||||
## Output Structure Reference
|
||||
|
||||
```
|
||||
output.iso
|
||||
└── xcdrom/ ← root ISO
|
||||
├── AUTORUN.INF ← auto-play CD (Windows)
|
||||
├── INDEX.PHP ← halaman web viewer
|
||||
├── README.TXT ← petunjuk penggunaan
|
||||
├── RUN.BAT ← batch file untuk jalanin viewer
|
||||
├── MICROD/ ← MicroDicom executable viewer
|
||||
│ ├── MDICOM.EXE ← main executable
|
||||
│ ├── MDICOM.CHM ← help file
|
||||
│ ├── MFC120U.DLL ← Visual C++ runtime
|
||||
│ ├── MSVCP120.DLL ← Visual C++ runtime
|
||||
│ ├── MSVCR120.DLL ← Visual C++ runtime
|
||||
│ ├── INDEX.PHP
|
||||
│ └── SETTINGS/ ← konfigurasi viewer
|
||||
│ ├── APPLICAT.XML
|
||||
│ ├── ANIMATIO.XML
|
||||
│ ├── ANNOTATI.XML
|
||||
│ ├── EXPORTDI.XML
|
||||
│ ├── EXPORTIM.XML
|
||||
│ ├── EXPORTVI.XML
|
||||
│ ├── OVERLAY.XML
|
||||
│ ├── OVERLAY_.XML
|
||||
│ ├── PRINT.XML
|
||||
│ └── WINDOWLE.XML
|
||||
└── DICOMDIR/ ← file DICOM pasien
|
||||
├── 26811B9D ← contoh file DICOM
|
||||
└── 601182D5 ← contoh file DICOM
|
||||
```
|
||||
126
docs/mkiso-analysis.md
Normal file
126
docs/mkiso-analysis.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# mkiso PHP Scripts — Analysis
|
||||
|
||||
## File Inventory
|
||||
|
||||
| File | Purpose | Output |
|
||||
|------|---------|--------|
|
||||
| `mkiso.php` | Retrieve DICOM studies by accession number from PACS, wrap in ISO with microdicom viewer, serve as download | ISO download |
|
||||
| `mkiso2.php` | Retrieve DICOM studies from PACS, then forward to another PACS (C-STORE relay) | DICOM forwarded to `172.16.0.120:104` |
|
||||
| `mkiso_multiple.php` | Multi-accession variant of mkiso.php, with patient-name lookup in DB, ISO download | ISO download with patient-name filename |
|
||||
|
||||
---
|
||||
|
||||
## Shared Flow (all three scripts)
|
||||
|
||||
```
|
||||
1. Parse accession_number from GET parameter
|
||||
2. Create temp working dir: /tmp/dicomdir_<uniqid>/
|
||||
3. Copy microdicom viewer files into temp dir (bootstrap DICOM viewer ISO)
|
||||
4. Fetch DICOM studies from PACS via Java dcm4che2 dcmqr (C-MOVE)
|
||||
5. [PACS is: ABPACS@localhost:11112, AE CDRECORD:10104 is the local receiver]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The Java Command — Deep Dive
|
||||
|
||||
```bash
|
||||
JAVA_HOME=/usr/lib/jvm/jdk1.8.0_144 LANG=en_US.iso-8859-1 \
|
||||
/usr/local/dcm4che/dcm4che2/bin/dcmqr \
|
||||
-L CDRECORD:10104 \
|
||||
ABPACS@localhost:11112 \
|
||||
-cmove CDRECORD \
|
||||
-qAccessionNumber=${accession_number} \
|
||||
-cstore ${cstore} \
|
||||
-cstoredest ${dicomdir}/DICOMDIR
|
||||
```
|
||||
|
||||
**This single Java process does FOUR things:**
|
||||
|
||||
| Role | Parameter | Description |
|
||||
|------|-----------|-------------|
|
||||
| **Storage SCP** | `-L CDRECORD:10104` | Listens as AE `CDRECORD` on port `10104` to receive incoming C-STORE from PACS |
|
||||
| **Query SCU** | `-qAccessionNumber=X` | Queries PACS for studies matching the accession number |
|
||||
| **C-MOVE SCU** | `-cmove CDRECORD` | Tells PACS to send matched studies to `CDRECORD` (itself) |
|
||||
| **File writer** | `-cstoredest DICOMDIR` | Saves received DICOM files to the specified directory |
|
||||
|
||||
**The `-cstore` parameter** specifies a DICOM SOP Class to negotiate. dcm4che2 maps the two-letter modality codes to SOP Class UIDs internally:
|
||||
|
||||
| Code | SOP Class | UID |
|
||||
|------|-----------|-----|
|
||||
| CR | Computed Radiography | 1.2.840.10008.5.1.4.1.1.1 |
|
||||
| CT | CT Image Storage | 1.2.840.10008.5.1.4.1.1.2 |
|
||||
| MR | MR Image Storage | 1.2.840.10008.5.1.4.1.1.4 |
|
||||
| US | Ultrasound Image | 1.2.840.10008.5.1.4.1.1.6.1 |
|
||||
| NM | Nuclear Medicine | 1.2.840.10008.5.1.4.1.1.20 |
|
||||
| PET | PET Image | 1.2.840.10008.5.1.4.1.1.128 |
|
||||
| SC | Secondary Capture | 1.2.840.10008.5.1.4.1.1.7 |
|
||||
| XA | X-Ray Angio | 1.2.840.10008.5.1.4.1.1.12.1 |
|
||||
| XRF | X-Ray Fluoroscopy | 1.2.840.10008.5.1.4.1.1.12.2 |
|
||||
| DX | Digital X-Ray | 1.2.840.10008.5.1.4.1.1.1.1 |
|
||||
| MG | Mammography | 1.2.840.10008.5.1.4.1.1.1.2 |
|
||||
| PR | Presentation State | 1.2.840.10008.5.1.4.1.1.11.1 |
|
||||
| KO | Key Object Selection | 1.2.840.10008.5.1.4.1.1.88.59 |
|
||||
| SR | Structured Report | 1.2.840.10008.5.1.4.1.1.88.11 |
|
||||
|
||||
**The loop iterates over ALL 14 modalities for the same accession number.** Most will return nothing — the script uses a brute-force "try everything" approach since it doesn't know which modality a given accession number uses.
|
||||
|
||||
---
|
||||
|
||||
## Per-Script Differences
|
||||
|
||||
### mkiso.php
|
||||
```
|
||||
Fetch → genisoimage → serve ISO download → cleanup
|
||||
```
|
||||
- ISO filename: `{accession_number}.iso`
|
||||
- After PACS fetch: creates ISO with genisoimage, streams it as download, deletes temp files
|
||||
|
||||
### mkiso2.php
|
||||
```
|
||||
Fetch → dcmsend to 172.16.0.120:104 → exit
|
||||
```
|
||||
- **No ISO creation.** Instead forwards retrieved DICOM files to another PACS server (`172.16.0.120:104`)
|
||||
- Uses `dcmsend +sd +rd 172.16.0.120 104 $dicomdir/DICOMDIR` (dcmtk `dcmsend` — a C-STORE SCU)
|
||||
- ISO creation and download code is commented out
|
||||
- This script acts as a **DICOM relay/proxy**: PACS A → temp dir → PACS B
|
||||
|
||||
### mkiso_multiple.php
|
||||
```
|
||||
Parse comma-separated ACC#s → DB lookup (patient name) →
|
||||
for each ACC#, for each modality: fetch from PACS →
|
||||
genisoimage → serve ISO download → cleanup
|
||||
```
|
||||
- Supports **comma-separated** accession numbers (e.g. `?accession_number=MR.001,CT.002`)
|
||||
- **Database lookup** on two DBs:
|
||||
- `pacsdb_his` (192.168.2.7) — finds `AccessionNumber` → `MEDRECID`, `RegID`
|
||||
- `pacsdb_his` — `MEDRECID` → `Nama` (patient name)
|
||||
- ISO filename: `{PATIENT_NAME}-{ACCESSION_LIST}.iso` (alphanumeric only)
|
||||
- Inner loop: for each accession number, try all 14 modalities
|
||||
|
||||
---
|
||||
|
||||
## DICOM Operations Summary
|
||||
|
||||
| Script | C-MOVE (PACS→local) | C-STORE (local→PACS2) | ISO gen | DB lookup |
|
||||
|--------|---------------------|----------------------|---------|-----------|
|
||||
| mkiso.php | ✅ 14 modality loop | ❌ | ✅ | ❌ |
|
||||
| mkiso2.php | ✅ 14 modality loop | ✅ (dcmsend) | ❌ | ❌ |
|
||||
| mkiso_multiple.php | ✅ 14 modality × N acc# | ❌ | ✅ | ✅ (patient name) |
|
||||
|
||||
---
|
||||
|
||||
## Environment
|
||||
|
||||
| Component | Version/Location |
|
||||
|-----------|-----------------|
|
||||
| dcm4che2 (Java) | `/usr/local/dcm4che/dcm4che2/bin/dcmqr` |
|
||||
| Java | JDK 1.8.0_144 (`/usr/lib/jvm/jdk1.8.0_144`) |
|
||||
| genisoimage | `/usr/bin/genisoimage` |
|
||||
| dcmsend | `/usr/bin/dcmsend` (dcmtk C-STORE SCU) |
|
||||
| dcmtk (static) | `/data/dcmtk-bin/` (v3.6.6) — findscu, movescu, storescp, getscu |
|
||||
| microdicom | `/var/www/html/microdicom/` (DICOM viewer bootstrap files) |
|
||||
| PACS source | `ABPACS@localhost:11112` |
|
||||
| Local AE | `CDRECORD:10104` |
|
||||
| PACS dest (mkiso2) | `172.16.0.120:104` |
|
||||
| DB (mkiso_multiple) | `192.168.2.7:3306` (pacsdb_his / rsabt201107) |
|
||||
191
docs/pacs_downloadiso-usage.md
Normal file
191
docs/pacs_downloadiso-usage.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# pacs_downloadiso Module — Integration Analysis
|
||||
|
||||
## Overview
|
||||
|
||||
The `pacs_downloadiso` module (under `/data/newsas-git/hisv2/his2_dev_raw/module/pacs_downloadiso/`) is the HIS frontend that drives the mkiso scripts. It runs inside the HIS web app and its JavaScript opens new browser windows pointing at scripts on the **PACS host**.
|
||||
|
||||
---
|
||||
|
||||
## Two-Server Architecture
|
||||
|
||||
```
|
||||
┌────────── HIS Server ──────────┐ ┌────── PACS Server ──────┐
|
||||
│ │ │ │
|
||||
│ pacs_downloadiso/ │ HTTP │ mkiso.php │
|
||||
│ index.php (UI) │ ──────→ │ mkiso_multiple.php │
|
||||
│ pacs_downloadiso.php (API) │ │ send_rimage_multiple.* │
|
||||
│ │ │ │
|
||||
│ hiscnf/config.php │ │ ABPACS:11112 │
|
||||
│ $_PACS_host (PACS server IP)│ │ CDRECORD:10104 │
|
||||
│ $_PACS │ │ │
|
||||
│ $_CDPUBLISHER │ │ /usr/local/dcm4che/ │
|
||||
│ │ │ /usr/bin/genisoimage │
|
||||
│ DB: rsabt201107 (192.168.2.7) │ │ /var/www/html/microdicom│
|
||||
│ trxdepartemen │ │ │
|
||||
│ pacs_result_series │ │ pacsdb_his:3306 │
|
||||
│ regpas / medrec │ │ (192.168.2.7) │
|
||||
└────────────────────────────────┘ └─────────────────────────┘
|
||||
```
|
||||
|
||||
- **HIS DB** (`rsabt201107`) — transaction data, patient registration, PACS result tracking
|
||||
- **PACS DB** (`pacsdb_his`) — `mkiso_multiple.php` queries it directly for patient name lookup via `AccessionNumber`
|
||||
- **Config key**: `$_PACS_host` in `hiscnf/config.php` tells the HIS frontend where the PACS web server is
|
||||
|
||||
---
|
||||
|
||||
## Endpoints Called by HIS Frontend
|
||||
|
||||
| HIS JS Function | HTTP Call | PACS Script | Parameters |
|
||||
|-----------------|-----------|-------------|------------|
|
||||
| `dowloadiso()` | `window.open` | `/mkiso.php` | `?accession_number=X` |
|
||||
| `printiso()` | `window.open` | `/send_rimage_multiple.php` | `?accession_number=X` |
|
||||
| `fn_downloadisomultiple()` | `window.open` | `/mkiso_multiple.php` | `?accession_number=X,Y,Z` (JS array .toString()) |
|
||||
| `fn_printisomultiple()` | `window.open` | `/send_rimage_multiple.php` | `?accession_number=X,Y,Z` (JS array .toString()) |
|
||||
|
||||
### Single vs Multi accession decision
|
||||
|
||||
The HIS UI determines which to call based on **how many AccessionNumbers a TrxDepartemenID has**:
|
||||
|
||||
- **1 AccessionNumber** → shows "Download ISO" button → calls `dowloadiso()` → `/mkiso.php?accession_number=X`
|
||||
- **>1 AccessionNumber** → shows "Download" link → opens multi dialog → calls `fn_downloadisomultiple()` → `/mkiso_multiple.php?accession_number=X,Y,Z`
|
||||
|
||||
The decision is made in `ambildatapreview()` (line ~163 in `pacs_downloadiso.php`):
|
||||
```php
|
||||
if($JumAccessionNo>1){
|
||||
$Action = "<a href='#' onclick=\"fn_bukadivdownloadmultiple('$row[TrxDepartemenID]')\">...";
|
||||
} else {
|
||||
$Action = "<a href='#' onclick=\"fn_bukadivdownload('$pr[AccessionNumber]')\">...";
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## mkiso2.php — NOT Called by HIS
|
||||
|
||||
**The `mkiso2.php` script is absent from all HIS integration points.** It was found in the local `/data/notes/my-task/mkiso/` directory but:
|
||||
|
||||
- Not referenced in `pacs_downloadiso/index.php` (JS)
|
||||
- Not referenced in `pacs_downloadiso/pacs_downloadiso.php` (PHP)
|
||||
- Not mentioned in any config
|
||||
|
||||
`mkiso2.php` is a **DICOM relay** that fetches from `ABPACS@localhost:11112` and forwards to `172.16.0.120:104` using `dcmsend`. It appears to be a standalone utility, possibly run manually or by cron.
|
||||
|
||||
**Action:** Mark mkiso2.php as lower priority in the migration todo.
|
||||
|
||||
---
|
||||
|
||||
## send_rimage_multiple.php — Out of Scope
|
||||
|
||||
Not present in local mkiso project. This is a **CD Publisher print script** on the PACS server. It's conditionally shown only when `$_CDPUBLISHER=='Y'`. Not part of the Java→dcmtk migration.
|
||||
|
||||
---
|
||||
|
||||
## AJAX API Flow (HIS Backend → PACS Host Resolution)
|
||||
|
||||
### 1. Single download path (`dowloadiso()`)
|
||||
|
||||
```
|
||||
User clicks "Download ISO"
|
||||
→ AJAX: pacs_downloadiso.php?exe=cek_data_accessionno&AccessionNumber=X
|
||||
→ PHP: checks pacs_result_series table for Published='Y'
|
||||
→ Response: { OK: "OK", PACS_HOST: "192.168.x.x" }
|
||||
→ JS: window.open("http://{PACS_HOST}/mkiso.php?accession_number=X")
|
||||
```
|
||||
|
||||
The `PACS_HOST` comes from `hiscnf/config.php` (`$_PACS_host`). The HIS module never constructs a URL with hardcoded PACS IP — it always resolves via AJAX or config.
|
||||
|
||||
### 2. Multi download path (`fn_downloadisomultiple()`)
|
||||
|
||||
```
|
||||
User clicks "Download ISO" (multi dialog)
|
||||
→ JS: get_config_pacs() — synchronous AJAX to pacs_downloadiso.php?exe=ambil_config_pacs
|
||||
→ Response: { PACS_HOST: "192.168.x.x" }
|
||||
→ JS: collects checked AccessionNumbers from flexigrid
|
||||
→ JS: window.open("http://{PACS_HOST}/mkiso_multiple.php?accession_number=X,Y,Z")
|
||||
```
|
||||
|
||||
### 3. Print ISO paths (`printiso()` / `fn_printisomultiple()`)
|
||||
|
||||
Same pattern but calling `/send_rimage_multiple.php` instead.
|
||||
|
||||
---
|
||||
|
||||
## Config Dependencies
|
||||
|
||||
The `pacs_downloadiso.php` backend uses these config variables (from `hiscnf/config.php`):
|
||||
|
||||
| Config Variable | Purpose | Used In |
|
||||
|----------------|---------|---------|
|
||||
| `$_PACS` | Must be `'1'` to enable the module | `cek_setting_pacs()`, `cek_data_accessionno()`, `ambil_config_pacs()` |
|
||||
| `$_PACS_host` | PACS server IP/hostname (where mkiso scripts live) | `cek_data_accessionno()`, `ambil_config_pacs()` |
|
||||
| `$_CDPUBLISHER` | If `'Y'`, show "Print ISO" button | `index.php` (inline PHP) |
|
||||
| `$_RAD_Workorder` | If `1`, filter by WorkOrder only | `ambildatapreview()` |
|
||||
| `$_trxdepartemen_verif` | If `'Y'`, exclude verification field from query | `ambildatapreview()` |
|
||||
|
||||
Also includes `hiscnf/pacsdownloadiso.config.php` — only used for `$_CDPUBLISHER`.
|
||||
|
||||
---
|
||||
|
||||
## Database Tables Involved
|
||||
|
||||
### HIS DB (`rsabt201107`)
|
||||
|
||||
| Table | Role |
|
||||
|-------|------|
|
||||
| `trxdepartemen` | Transaction header — RegID, TrxID, Tanggal, DepartemenID, WorkOrder |
|
||||
| `trxlayanan` | Links TrxLayananID → TrxDepartemenID |
|
||||
| `regpas` | Patient registration — Nama, MEDRECID |
|
||||
| `medrec` | Medical records — Nama (patient name) |
|
||||
| `departemen` | Department — Nama, ModulExternal, NamaModulExternal |
|
||||
| `dokter` | Doctor — Nama |
|
||||
| `masterlayanan` | Service catalog — Nama, ModalityCode |
|
||||
| `pacs_result_series` | PACS result tracking — AccessionNumber, Published, TrxDepartemenID |
|
||||
| `pacs_order_mwl` | PACS worklist — AccessionNumber, TrxDepartemenID |
|
||||
|
||||
### PACS DB (`pacsdb_his` on 192.168.2.7)
|
||||
|
||||
| Table | Use by mkiso_multiple.php |
|
||||
|-------|---------------------------|
|
||||
| `pacs_result_series` | Lookup `MEDRECID`, `RegID` from `AccessionNumber` |
|
||||
| `medrec` | Lookup `Nama` (patient name) from `MEDRECID` |
|
||||
|
||||
---
|
||||
|
||||
## Key Observations for dcmtk Migration
|
||||
|
||||
### 1. `PACS_HOST` is a config variable — must update on the PACS server only
|
||||
|
||||
The HIS module points to whatever `$_PACS_host` is. **No code changes needed in the HIS module** for the mkiso migration. All changes are on the PACS server where `mkiso.php`, `mkiso_multiple.php`, and `mkiso2.php` live.
|
||||
|
||||
### 2. Concurrent requests are possible
|
||||
|
||||
Multiple HIS users might click "Download ISO" at once. The current Java-based approach binds to **port 10104** (hardcoded). With dcmtk `storescp`, you need a unique port per request to avoid conflicts.
|
||||
|
||||
### 3. mkiso2.php is standalone — can be batched
|
||||
|
||||
Not called by HIS. Can be migrated independently or de-prioritized.
|
||||
|
||||
### 4. send_rimage_multiple.php out of scope
|
||||
|
||||
Not part of the mkiso scripts. Leave as-is.
|
||||
|
||||
### 5. No changes to HIS module needed
|
||||
|
||||
The AJAX calls pass `AccessionNumber` as a query parameter to scripts on the PACS host. The mkiso scripts' **external interface** (GET parameter `accession_number`) does not change. Only the internal DICOM fetching mechanism changes.
|
||||
|
||||
### 6. Multi-accession parameter format
|
||||
|
||||
`mkiso_multiple.php` receives `?accession_number=X,Y,Z` (comma-separated). The `mkiso_multiple.php` script already handles this format via `explode(",", ...)`. No change needed.
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist (Post-Migration)
|
||||
|
||||
- [ ] HIS preview still shows PACS studies with correct Status and Download buttons
|
||||
- [ ] Single accession download: `window.open` to `/mkiso.php?accession_number=X` works
|
||||
- [ ] Multi accession download: `window.open` to `/mkiso_multiple.php?accession_number=X,Y,Z` works
|
||||
- [ ] Print ISO (CD Publisher) still works via `/send_rimage_multiple.php`
|
||||
- [ ] Config check: `cek_setting_pacs()` returns OK
|
||||
- [ ] AJAX: `cek_data_accessionno()` returns correct PACS_HOST and OK status
|
||||
- [ ] AJAX: `ambil_config_pacs()` returns correct PACS_HOST
|
||||
- [ ] No PHP errors in pacs_downloadiso.php after migration
|
||||
235
docs/patient-api-spec.md
Normal file
235
docs/patient-api-spec.md
Normal file
@@ -0,0 +1,235 @@
|
||||
# Patient Data API — External Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
The Go mkiso server needs patient data (name, medical record ID, registration ID) to generate ISO filenames. Instead of connecting directly to the HIS/PACS database, it calls this API.
|
||||
|
||||
**This API must be implemented by the HIS/PACS team** (or provided as a thin proxy in front of the existing DB).
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
Use one of:
|
||||
- `Authorization: Bearer <jwt-token>` — if the HIS exposes standard JWT
|
||||
- `X-API-Key: <key>` — simpler for internal services
|
||||
- Configurable in Go server's `config.yaml`:`patient_api.auth_header`, `patient_api.auth_token`
|
||||
|
||||
---
|
||||
|
||||
## Endpoint: Get Patient by Accession Number
|
||||
|
||||
### `GET /patient/by-accession`
|
||||
|
||||
**Query Parameters:**
|
||||
|
||||
| Parameter | Required | Type | Description |
|
||||
|-----------|----------|------|-------------|
|
||||
| `accession_number` | Yes | string | DICOM Accession Number (e.g., `MR.2024.001`) |
|
||||
|
||||
**Request:**
|
||||
```
|
||||
GET /patient/by-accession?accession_number=MR.2024.001
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
|
||||
```
|
||||
|
||||
**Response 200 — OK:**
|
||||
|
||||
```json
|
||||
{
|
||||
"accession_number": "MR.2024.001",
|
||||
"medrec_id": "00012345",
|
||||
"reg_id": "REG-2024-00123",
|
||||
"patient_name": "JOHN DOE",
|
||||
"modality": "MR",
|
||||
"study_description": "MRI Brain"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `accession_number` | string | Echoed back for verification |
|
||||
| `medrec_id` | string | Medical Record ID |
|
||||
| `reg_id` | string | Registration ID |
|
||||
| `patient_name` | string | Patient full name (for ISO filename) |
|
||||
| `modality` | string | DICOM Modality code (MR, CT, CR, etc.) |
|
||||
| `study_description` | string | DICOM Study Description |
|
||||
|
||||
**Response 404 — Not Found:**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "accession_number not found",
|
||||
"accession_number": "MR.9999.XXX"
|
||||
}
|
||||
```
|
||||
|
||||
**Response 502 — Upstream Failure:**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "upstream database unavailable"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Guide (for the HIS/PACS team)
|
||||
|
||||
### Data Source
|
||||
|
||||
The existing `mkiso_multiple.php` queries these tables directly:
|
||||
|
||||
```sql
|
||||
-- Step 1: MEDRECID + RegID from pacs_result_series
|
||||
SELECT MEDRECID, RegID
|
||||
FROM pacs_result_series
|
||||
WHERE AccessionNumber = ?
|
||||
LIMIT 1;
|
||||
|
||||
-- Step 2: Patient name from medrec
|
||||
SELECT Nama
|
||||
FROM medrec
|
||||
WHERE MEDRECID = ?;
|
||||
```
|
||||
|
||||
**The API should wrap these queries** but the Go server never touches the DB directly.
|
||||
|
||||
### Minimal Reference Implementation (PHP)
|
||||
|
||||
```php
|
||||
<?php
|
||||
// patient-api.php
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Auth check (simple API key)
|
||||
$valid_key = getenv('PATIENT_API_KEY');
|
||||
if ($_SERVER['HTTP_X_API_KEY'] !== $valid_key) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$acc = $_GET['accession_number'] ?? '';
|
||||
if (empty($acc)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'missing accession_number']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Connect to DB (PACS DB: 192.168.2.7, pacsdb_his)
|
||||
$db = new PDO('mysql:host=192.168.2.7;dbname=pacsdb_his', 'remote', '12Digit');
|
||||
|
||||
$stmt = $db->prepare(
|
||||
"SELECT prs.MEDRECID, prs.RegID, m.Nama
|
||||
FROM pacs_result_series prs
|
||||
LEFT JOIN medrec m ON m.MEDRECID = prs.MEDRECID
|
||||
WHERE prs.AccessionNumber = ?
|
||||
LIMIT 1"
|
||||
);
|
||||
$stmt->execute([$acc]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$row) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'accession_number not found', 'accession_number' => $acc]);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'accession_number' => $acc,
|
||||
'medrec_id' => $row['MEDRECID'],
|
||||
'reg_id' => $row['RegID'],
|
||||
'patient_name' => $row['Nama'],
|
||||
'modality' => '', // Optional, can be added from another query
|
||||
'study_description' => '' // Optional
|
||||
]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Go Server Configuration for Patient API
|
||||
|
||||
```yaml
|
||||
# config.yaml
|
||||
patient_api:
|
||||
base_url: "http://192.168.2.7:8090" # Patient API server
|
||||
endpoint: "/patient/by-accession"
|
||||
auth_type: "api_key" # "jwt" or "api_key" or "none"
|
||||
auth_header: "X-API-Key" # Header name for auth
|
||||
auth_token: "changeme-secret-key" # The API key or JWT
|
||||
timeout: 10s
|
||||
retry: 3 # Retry on network failure
|
||||
retry_backoff: 500ms
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Go Client (internal/repo/patient.go)
|
||||
|
||||
```go
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PatientData struct {
|
||||
AccessionNumber string `json:"accession_number"`
|
||||
MedrecID string `json:"medrec_id"`
|
||||
RegID string `json:"reg_id"`
|
||||
PatientName string `json:"patient_name"`
|
||||
Modality string `json:"modality"`
|
||||
StudyDescription string `json:"study_description"`
|
||||
}
|
||||
|
||||
type PatientRepo struct {
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
authHeader string
|
||||
authToken string
|
||||
}
|
||||
|
||||
func NewPatientRepo(baseURL, authHeader, authToken string, timeout time.Duration) *PatientRepo {
|
||||
return &PatientRepo{
|
||||
baseURL: baseURL,
|
||||
httpClient: &http.Client{Timeout: timeout},
|
||||
authHeader: authHeader,
|
||||
authToken: authToken,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *PatientRepo) ByAccessionNumber(ctx context.Context, acc string) (*PatientData, error) {
|
||||
u, _ := url.Parse(r.baseURL + "/patient/by-accession")
|
||||
u.RawQuery = url.Values{"accession_number": {acc}}.Encode()
|
||||
|
||||
req, _ := http.NewRequestWithContext(ctx, "GET", u.String(), nil)
|
||||
if r.authHeader != "" {
|
||||
req.Header.Set(r.authHeader, r.authToken)
|
||||
}
|
||||
|
||||
resp, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("patient API request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
return nil, fmt.Errorf("accession_number %q not found", acc)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("patient API returned %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var data PatientData
|
||||
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||
return nil, fmt.Errorf("patient API response decode: %w", err)
|
||||
}
|
||||
return &data, nil
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user