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

186 lines
5.2 KiB
Go

package service
import (
"context"
"fmt"
"log/slog"
"os"
"path/filepath"
"regexp"
"strings"
"mkiso-server/internal/config"
"mkiso-server/internal/isobuilder"
"mkiso-server/internal/repo"
"mkiso-server/pkg/dicom"
)
// ISOService handles ISO creation from DICOM data.
type ISOService struct {
cfg *config.Config
dicomSvc *DicomService
patientRepo *repo.PatientRepo
}
// NewISOService creates a new ISOService.
func NewISOService(cfg *config.Config, dicomSvc *DicomService, patientRepo *repo.PatientRepo) *ISOService {
return &ISOService{
cfg: cfg,
dicomSvc: dicomSvc,
patientRepo: patientRepo,
}
}
// ISOItem is the result of ISO generation.
type ISOItem struct {
Path string // Full path to the generated ISO file
Cleanup func() // Call to remove temp files
Filename string // Suggested download filename
}
// GenerateISO creates an ISO for a single accession number.
func (s *ISOService) GenerateISO(ctx context.Context, accessionNumber string) (*ISOItem, error) {
// Create temp working directory
tempDir, err := os.MkdirTemp(s.cfg.ISO.TempDir, "dicomdir_")
if err != nil {
return nil, fmt.Errorf("create temp dir: %w", err)
}
cleanup := func() { os.RemoveAll(tempDir) }
dicomDir := filepath.Join(tempDir, "DICOMDIR")
if err := os.MkdirAll(dicomDir, 0755); err != nil {
cleanup()
return nil, fmt.Errorf("create DICOMDIR: %w", err)
}
// Copy microdicom viewer files
if err := s.copyMicrodicom(tempDir); err != nil {
cleanup()
return nil, fmt.Errorf("copy microdicom: %w", err)
}
// Fetch DICOM from PACS
_, err = s.dicomSvc.FetchDICOM(ctx, accessionNumber, dicomDir)
if err != nil {
cleanup()
return nil, fmt.Errorf("DICOM fetch failed: %w", err)
}
// Generate ISO
isoName := sanitizeFilename(accessionNumber) + ".iso"
isoPath := filepath.Join(s.cfg.ISO.TempDir, isoName)
if err := isobuilder.BuildFromDirectory(tempDir, isoPath, "DICOM"); err != nil {
cleanup()
os.Remove(isoPath)
return nil, fmt.Errorf("ISO creation failed: %w", err)
}
slog.Info("ISO created",
"path", isoPath,
"accession", accessionNumber,
)
return &ISOItem{
Path: isoPath,
Cleanup: func() { cleanup(); os.Remove(isoPath) },
Filename: isoName,
}, nil
}
// GenerateISOMultiple creates an ISO for multiple accession numbers.
// It tries to get the patient name from the patient API for a descriptive filename.
// If the patient API is unavailable, it falls back to the accession list as filename.
func (s *ISOService) GenerateISOMultiple(ctx context.Context, accessionNumbers []string) (*ISOItem, error) {
// Create temp working directory
tempDir, err := os.MkdirTemp(s.cfg.ISO.TempDir, "dicomdir_")
if err != nil {
return nil, fmt.Errorf("create temp dir: %w", err)
}
cleanup := func() { os.RemoveAll(tempDir) }
dicomDir := filepath.Join(tempDir, "DICOMDIR")
if err := os.MkdirAll(dicomDir, 0755); err != nil {
cleanup()
return nil, fmt.Errorf("create DICOMDIR: %w", err)
}
// Copy microdicom viewer files
if err := s.copyMicrodicom(tempDir); err != nil {
cleanup()
return nil, fmt.Errorf("copy microdicom: %w", err)
}
// Fetch DICOM from PACS
_, err = s.dicomSvc.FetchDICOMMultiple(ctx, accessionNumbers, dicomDir)
if err != nil {
cleanup()
return nil, fmt.Errorf("DICOM fetch failed: %w", err)
}
// Determine filename: try patient API, fall back to accession list
filename := buildMultiFilename(accessionNumbers)
patient, err := s.patientRepo.ByAccessionNumber(ctx, accessionNumbers[0])
if err != nil {
slog.Warn("patient API lookup failed, using accession-based filename",
"accession", accessionNumbers[0],
"error", err,
)
} else if patient != nil && patient.PatientName != "" {
name := sanitizeFilename(patient.PatientName)
accPart := sanitizeFilename(accessionList(accessionNumbers))
filename = fmt.Sprintf("%s-%s.iso", name, accPart)
slog.Info("using patient name for ISO filename",
"patient", patient.PatientName,
"filename", filename,
)
}
isoPath := filepath.Join(s.cfg.ISO.TempDir, filename)
if err := isobuilder.BuildFromDirectory(tempDir, isoPath, "DICOM"); err != nil {
cleanup()
os.Remove(isoPath)
return nil, fmt.Errorf("ISO creation failed: %w", err)
}
slog.Info("ISO created (multiple)",
"path", isoPath,
"accessions", accessionNumbers,
)
return &ISOItem{
Path: isoPath,
Cleanup: func() { cleanup(); os.Remove(isoPath) },
Filename: filename,
}, nil
}
// copyMicrodicom copies the microdicom viewer files into the temp directory.
func (s *ISOService) copyMicrodicom(destDir string) error {
if !dicom.DirExists(s.cfg.ISO.MicrodicomPath) {
slog.Warn("microdicom path does not exist, skipping copy",
"path", s.cfg.ISO.MicrodicomPath,
)
return nil
}
return dicom.CopyDir(s.cfg.ISO.MicrodicomPath, destDir)
}
// sanitizeFilename removes characters not safe for filenames.
func sanitizeFilename(s string) string {
reg := regexp.MustCompile(`[^a-zA-Z0-9\-\.]+`)
return strings.TrimRight(reg.ReplaceAllString(strings.ToUpper(s), ""), ".-")
}
// accessionList joins accession numbers with "-".
func accessionList(accs []string) string {
return strings.Join(accs, "-")
}
// buildMultiFilename creates a default filename from accession numbers.
func buildMultiFilename(accs []string) string {
return sanitizeFilename(accessionList(accs)) + ".iso"
}