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 }