fix: findscu go mkiso
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -78,31 +79,43 @@ func (s *DicomService) FetchDICOMMultiple(ctx context.Context, accessionNumbers
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
exitCode, _, stderr, err := dicom.RunMoveSCU(
|
studyUIDs, err := s.findStudyUIDs(ctx, acc)
|
||||||
ctx,
|
|
||||||
s.cfg.DCMTK.Movescu,
|
|
||||||
s.cfg.OurAE.AETitle,
|
|
||||||
s.cfg.PACS.AETitle,
|
|
||||||
s.cfg.PACS.Host,
|
|
||||||
s.cfg.PACS.Port,
|
|
||||||
port,
|
|
||||||
acc,
|
|
||||||
300, // 5 min timeout
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("movescu failed for accession",
|
slog.Warn("findscu failed for accession",
|
||||||
"accession", acc,
|
"accession", acc,
|
||||||
"exit_code", exitCode,
|
|
||||||
"stderr", stderr,
|
|
||||||
"error", err,
|
"error", err,
|
||||||
)
|
)
|
||||||
// Continue with next accession — partial success is acceptable
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
slog.Info("movescu completed",
|
|
||||||
"accession", acc,
|
for _, studyUID := range studyUIDs {
|
||||||
"exit_code", exitCode,
|
exitCode, _, stderr, err := dicom.RunMoveSCUByStudyUID(
|
||||||
)
|
ctx,
|
||||||
|
s.cfg.DCMTK.Movescu,
|
||||||
|
s.cfg.OurAE.AETitle,
|
||||||
|
s.cfg.PACS.AETitle,
|
||||||
|
s.cfg.PACS.Host,
|
||||||
|
s.cfg.PACS.Port,
|
||||||
|
port,
|
||||||
|
studyUID,
|
||||||
|
300, // 5 min timeout
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("movescu failed for study uid",
|
||||||
|
"accession", acc,
|
||||||
|
"study_uid", studyUID,
|
||||||
|
"exit_code", exitCode,
|
||||||
|
"stderr", stderr,
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
slog.Info("movescu completed",
|
||||||
|
"accession", acc,
|
||||||
|
"study_uid", studyUID,
|
||||||
|
"exit_code", exitCode,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count files retrieved
|
// Count files retrieved
|
||||||
@@ -140,26 +153,33 @@ func (s *DicomService) fetchDICOMWithPort(ctx context.Context, accessionNumber,
|
|||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run movescu
|
studyUIDs, err := s.findStudyUIDs(ctx, accessionNumber)
|
||||||
exitCode, _, stderr, err := dicom.RunMoveSCU(
|
|
||||||
ctx,
|
|
||||||
s.cfg.DCMTK.Movescu,
|
|
||||||
s.cfg.OurAE.AETitle,
|
|
||||||
s.cfg.PACS.AETitle,
|
|
||||||
s.cfg.PACS.Host,
|
|
||||||
s.cfg.PACS.Port,
|
|
||||||
port,
|
|
||||||
accessionNumber,
|
|
||||||
300, // 5 min timeout
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("movescu failed (exit %d): %s (stderr: %s)", exitCode, err, stderr)
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("movescu completed",
|
for _, studyUID := range studyUIDs {
|
||||||
"accession", accessionNumber,
|
exitCode, _, stderr, err := dicom.RunMoveSCUByStudyUID(
|
||||||
"exit_code", exitCode,
|
ctx,
|
||||||
)
|
s.cfg.DCMTK.Movescu,
|
||||||
|
s.cfg.OurAE.AETitle,
|
||||||
|
s.cfg.PACS.AETitle,
|
||||||
|
s.cfg.PACS.Host,
|
||||||
|
s.cfg.PACS.Port,
|
||||||
|
port,
|
||||||
|
studyUID,
|
||||||
|
300, // 5 min timeout
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("movescu failed for study uid %q (exit %d): %s (stderr: %s)", studyUID, exitCode, err, stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("movescu completed",
|
||||||
|
"accession", accessionNumber,
|
||||||
|
"study_uid", studyUID,
|
||||||
|
"exit_code", exitCode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Count files
|
// Count files
|
||||||
filesCount, err = countFiles(destDir)
|
filesCount, err = countFiles(destDir)
|
||||||
@@ -174,6 +194,24 @@ func (s *DicomService) fetchDICOMWithPort(ctx context.Context, accessionNumber,
|
|||||||
return filesCount, nil
|
return filesCount, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *DicomService) findStudyUIDs(ctx context.Context, accessionNumber string) ([]string, error) {
|
||||||
|
findscuBin := filepath.Join(filepath.Dir(s.cfg.DCMTK.Movescu), "findscu")
|
||||||
|
studyUIDs, _, stderr, err := dicom.RunFindSCUStudyUIDs(
|
||||||
|
ctx,
|
||||||
|
findscuBin,
|
||||||
|
s.cfg.OurAE.AETitle,
|
||||||
|
s.cfg.PACS.AETitle,
|
||||||
|
s.cfg.PACS.Host,
|
||||||
|
s.cfg.PACS.Port,
|
||||||
|
accessionNumber,
|
||||||
|
60,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("findscu failed for accession %q: %w (stderr: %s)", accessionNumber, err, stderr)
|
||||||
|
}
|
||||||
|
return studyUIDs, nil
|
||||||
|
}
|
||||||
|
|
||||||
// waitForStorescpReady waits for storescp to start or detects early failure.
|
// waitForStorescpReady waits for storescp to start or detects early failure.
|
||||||
func waitForStorescpReady(resultCh <-chan dicom.StorescpResult) error {
|
func waitForStorescpReady(resultCh <-chan dicom.StorescpResult) error {
|
||||||
select {
|
select {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -101,17 +102,72 @@ func StartStoresCP(ctx context.Context, bin, aeTitle string, port int, outputDir
|
|||||||
return ch, stop, nil
|
return ch, stop, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunMoveSCU executes movescu for the given accession number.
|
// RunFindSCUStudyUIDs queries PACS by accession number and returns matching StudyInstanceUID values.
|
||||||
|
func RunFindSCUStudyUIDs(ctx context.Context, bin, ourAE, pacsAE, pacsHost string, pacsPort int, accessionNumber string, timeoutSec int) (studyUIDs []string, stdout, stderr string, err error) {
|
||||||
|
args := []string{
|
||||||
|
"-v",
|
||||||
|
"-aet", ourAE,
|
||||||
|
"-aec", pacsAE,
|
||||||
|
pacsHost, strconv.Itoa(pacsPort),
|
||||||
|
"-S",
|
||||||
|
"-k", "0008,0052=STUDY",
|
||||||
|
"-k", fmt.Sprintf("0008,0050=%s", accessionNumber),
|
||||||
|
"-k", "0020,000D",
|
||||||
|
}
|
||||||
|
|
||||||
|
if timeoutSec > 0 {
|
||||||
|
args = append(args, "-to", strconv.Itoa(timeoutSec))
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("running findscu",
|
||||||
|
"bin", bin,
|
||||||
|
"accession", accessionNumber,
|
||||||
|
"pacs", fmt.Sprintf("%s@%s:%d", pacsAE, pacsHost, pacsPort),
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, bin, args...)
|
||||||
|
var outBuf, errBuf bytes.Buffer
|
||||||
|
cmd.Stdout = &outBuf
|
||||||
|
cmd.Stderr = &errBuf
|
||||||
|
|
||||||
|
err = cmd.Run()
|
||||||
|
stdout = outBuf.String()
|
||||||
|
stderr = errBuf.String()
|
||||||
|
combined := stdout + "\n" + stderr
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, stdout, stderr, fmt.Errorf("findscu failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
uidRe := regexp.MustCompile(`\(0020,000d\) UI \[([^\]]+)\]`)
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
for _, match := range uidRe.FindAllStringSubmatch(combined, -1) {
|
||||||
|
uid := strings.TrimSpace(match[1])
|
||||||
|
if uid == "" || seen[uid] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[uid] = true
|
||||||
|
studyUIDs = append(studyUIDs, uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(studyUIDs) == 0 {
|
||||||
|
return nil, stdout, stderr, fmt.Errorf("no StudyInstanceUID found for accession %q", accessionNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
return studyUIDs, stdout, stderr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunMoveSCUByStudyUID executes movescu for the given StudyInstanceUID.
|
||||||
// It uses Study Root query/retrieve model (via -S flag).
|
// It uses Study Root query/retrieve model (via -S flag).
|
||||||
func RunMoveSCU(ctx context.Context, bin, ourAE, pacsAE, pacsHost string, pacsPort, moveDestPort int, accessionNumber string, timeoutSec int) (exitCode int, stdout, stderr string, err error) {
|
func RunMoveSCUByStudyUID(ctx context.Context, bin, ourAE, pacsAE, pacsHost string, pacsPort, moveDestPort int, studyUID string, timeoutSec int) (exitCode int, stdout, stderr string, err error) {
|
||||||
args := []string{
|
args := []string{
|
||||||
"-aet", ourAE,
|
"-aet", ourAE,
|
||||||
"-aec", pacsAE,
|
"-aec", pacsAE,
|
||||||
"-aem", ourAE,
|
"-aem", ourAE,
|
||||||
pacsHost, strconv.Itoa(pacsPort),
|
pacsHost, strconv.Itoa(pacsPort),
|
||||||
"-S", // Study Root query/retrieve
|
"-S", // Study Root query/retrieve
|
||||||
"-k", fmt.Sprintf("0008,0050=%s", accessionNumber),
|
"-k", "0008,0052=STUDY",
|
||||||
"-k", "0010,0020=", // Patient ID wildcard (required for Study Root)
|
"-k", fmt.Sprintf("0020,000D=%s", studyUID),
|
||||||
"--no-port",
|
"--no-port",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +177,7 @@ func RunMoveSCU(ctx context.Context, bin, ourAE, pacsAE, pacsHost string, pacsPo
|
|||||||
|
|
||||||
slog.Info("running movescu",
|
slog.Info("running movescu",
|
||||||
"bin", bin,
|
"bin", bin,
|
||||||
"accession", accessionNumber,
|
"study_uid", studyUID,
|
||||||
"pacs", fmt.Sprintf("%s@%s:%d", pacsAE, pacsHost, pacsPort),
|
"pacs", fmt.Sprintf("%s@%s:%d", pacsAE, pacsHost, pacsPort),
|
||||||
"dest", fmt.Sprintf("%s:%d", ourAE, moveDestPort),
|
"dest", fmt.Sprintf("%s:%d", ourAE, moveDestPort),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user