feat: base go mkiso
This commit is contained in:
185
internal/service/iso.go
Normal file
185
internal/service/iso.go
Normal file
@@ -0,0 +1,185 @@
|
||||
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"
|
||||
}
|
||||
Reference in New Issue
Block a user