Files
dicom-iso/internal/service/relay.go
2026-06-05 08:11:44 +07:00

121 lines
3.5 KiB
Go

package service
import (
"context"
"fmt"
"log/slog"
"os"
"path/filepath"
"mkiso-server/internal/config"
"mkiso-server/internal/repo"
"mkiso-server/pkg/dicom"
)
// RelayService handles DICOM relay (print) to the CD Publisher.
type RelayService struct {
cfg *config.Config
dicomSvc *DicomService
patientRepo *repo.PatientRepo
}
// NewRelayService creates a new RelayService.
func NewRelayService(cfg *config.Config, dicomSvc *DicomService, patientRepo *repo.PatientRepo) *RelayService {
return &RelayService{
cfg: cfg,
dicomSvc: dicomSvc,
patientRepo: patientRepo,
}
}
// Destination returns the CD Publisher address as "host:port".
func (s *RelayService) Destination() string {
return fmt.Sprintf("%s:%d", s.cfg.CDPublisher.Host, s.cfg.CDPublisher.Port)
}
// RelayResult describes the outcome of a DICOM relay operation.
type RelayResult struct {
AccessionsSent []string `json:"accessions_sent"`
PatientName string `json:"patient_name"`
Destination string `json:"destination"`
FilesSent int `json:"files_sent"`
}
// RelayToCDPublisher fetches DICOM studies from PACS and forwards them
// to the CD Publisher via storescu (C-STORE).
func (s *RelayService) RelayToCDPublisher(ctx context.Context, accessionNumbers []string) (*RelayResult, error) {
// Step 1: Try to get patient data from API (graceful if unavailable)
var patientName string
patient, err := s.patientRepo.ByAccessionNumber(ctx, accessionNumbers[0])
if err != nil {
slog.Warn("patient API lookup failed for relay",
"accession", accessionNumbers[0],
"error", err,
)
} else if patient != nil {
patientName = patient.PatientName
slog.Info("patient data retrieved for relay",
"accession", accessionNumbers[0],
"patient", patientName,
)
}
// Step 2: Create temp dir
tempDir, err := os.MkdirTemp(s.cfg.ISO.TempDir, "dicomdir_")
if err != nil {
return nil, fmt.Errorf("create temp dir: %w", err)
}
defer os.RemoveAll(tempDir) // guaranteed cleanup
dicomDir := filepath.Join(tempDir, "DICOMDIR")
if err := os.MkdirAll(dicomDir, 0755); err != nil {
return nil, fmt.Errorf("create DICOMDIR: %w", err)
}
// Copy microdicom viewer files (optional — CD Publisher doesn't need it,
// but matches PHP behavior of copying before fetch)
if dicom.DirExists(s.cfg.ISO.MicrodicomPath) {
if err := dicom.CopyDir(s.cfg.ISO.MicrodicomPath, tempDir); err != nil {
slog.Warn("copy microdicom for relay failed", "error", err)
}
}
// Step 3: Fetch DICOM from PACS
filesRetrieved, err := s.dicomSvc.FetchDICOMMultiple(ctx, accessionNumbers, dicomDir)
if err != nil {
return nil, fmt.Errorf("DICOM fetch failed: %w", err)
}
slog.Info("DICOM fetched for relay",
"accessions", accessionNumbers,
"files", filesRetrieved,
)
// Step 4: Relay to CD Publisher via storescu
destination := fmt.Sprintf("%s:%d", s.cfg.CDPublisher.Host, s.cfg.CDPublisher.Port)
exitCode, stdout, stderr, err := dicom.RunStoreSCU(
ctx,
s.cfg.DCMTK.Storescu,
s.cfg.OurAE.AETitle,
s.cfg.CDPublisher.Host,
s.cfg.CDPublisher.Port,
dicomDir,
)
if err != nil {
return nil, fmt.Errorf("storescu relay failed (exit %d): %w (stderr: %s)", exitCode, err, stderr)
}
_ = stdout
slog.Info("DICOM relayed to CD Publisher",
"accessions", accessionNumbers,
"destination", destination,
"files", filesRetrieved,
)
return &RelayResult{
AccessionsSent: accessionNumbers,
PatientName: patientName,
Destination: destination,
FilesSent: filesRetrieved,
}, nil
}