186 lines
5.2 KiB
Go
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"
|
|
}
|