Files
dicom-iso/todo/02-genisoimage-to-godiskfs.detail.md
2026-06-05 08:11:44 +07:00

6.3 KiB

Detail: genisoimage → go-diskfs

1. internal/isobuilder/builder.go — new file

package isobuilder

import (
    "fmt"
    "io"
    "os"
    "path/filepath"

    diskfs "github.com/diskfs/go-diskfs"
    "github.com/diskfs/go-diskfs/disk"
    "github.com/diskfs/go-diskfs/filesystem"
    "github.com/diskfs/go-diskfs/filesystem/iso9660"
)

// BuildFromDirectory creates an ISO 9660 image from a source directory.
// Equivalent to: genisoimage -iso-level 4 -r -J -V <label> -o <isoPath> <srcDir>
func BuildFromDirectory(srcDir, isoPath, volumeLabel string) error {
    // Step 1: Calculate total size (sum of all files + 10% overhead)
    totalSize, err := dirSize(srcDir)
    if err != nil {
        return fmt.Errorf("calculate directory size: %w", err)
    }

    // Step 2: Create disk image
    mydisk, err := diskfs.Create(isoPath, totalSize, diskfs.SectorSizeDefault)
    if err != nil {
        return fmt.Errorf("create disk image: %w", err)
    }
    mydisk.LogicalBlocksize = 2048

    // Step 3: Create ISO 9660 filesystem
    fs, err := mydisk.CreateFilesystem(disk.FilesystemSpec{
        Partition:   0,
        FSType:      filesystem.TypeISO9660,
        VolumeLabel: volumeLabel,
    })
    if err != nil {
        return fmt.Errorf("create ISO filesystem: %w", err)
    }
    defer fs.Close()

    // Step 4: Walk source dir and copy files
    err = filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }

        relPath, err := filepath.Rel(srcDir, path)
        if err != nil {
            return err
        }

        if relPath == "." {
            return nil // skip root
        }

        if info.IsDir() {
            if err := fs.Mkdir(relPath); err != nil {
                return fmt.Errorf("mkdir %q: %w", relPath, err)
            }
            return nil
        }

        // Regular file — copy contents
        rw, err := fs.OpenFile(relPath, os.O_CREATE|os.O_RDWR)
        if err != nil {
            return fmt.Errorf("open file %q: %w", relPath, err)
        }
        defer rw.Close()

        srcFile, err := os.Open(path)
        if err != nil {
            return fmt.Errorf("open source %q: %w", path, err)
        }
        defer srcFile.Close()

        if _, err := io.Copy(rw, srcFile); err != nil {
            return fmt.Errorf("copy %q: %w", relPath, err)
        }

        return nil
    })
    if err != nil {
        return fmt.Errorf("walk source directory: %w", err)
    }

    // Step 5: Finalize with Rock Ridge + Joliet
    iso, ok := fs.(*iso9660.FileSystem)
    if !ok {
        return fmt.Errorf("not an ISO 9660 filesystem")
    }
    if err := iso.Finalize(iso9660.FinalizeOptions{
        RockRidge:        true,
        Joliet:           true,
        DeepDirectories:  true,
        VolumeIdentifier: volumeLabel,
    }); err != nil {
        return fmt.Errorf("finalize ISO: %w", err)
    }

    return nil
}

// dirSize calculates total size of all files in a directory tree,
// with 10% overhead margin for ISO metadata.
func dirSize(dir string) (int64, error) {
    var total int64
    err := filepath.Walk(dir, func(_ string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }
        if !info.IsDir() {
            total += info.Size()
        }
        return nil
    })
    if err != nil {
        return 0, err
    }
    // Add 10% overhead for ISO 9660 metadata
    total = total + total/10
    // Minimum 10MB (some PACS studies are small)
    if total < 10*1024*1024 {
        total = 10 * 1024 * 1024
    }
    // Round up to next 2048-byte sector
    if total%2048 != 0 {
        total = total + (2048 - total%2048)
    }
    return total, nil
}

2. internal/config/config.go — remove Tools field

Current (line ~31):

type Config struct {
    Server      ServerConfig      `yaml:"server"`
    Auth        AuthConfig        `yaml:"auth"`
    DCMTK       DCMTKConfig       `yaml:"dcmtk"`
    Tools       ToolsConfig       `yaml:"tools"`
    ...
}

Remove Tools ToolsConfig field and entire ToolsConfig struct.

Current validation (line ~82):

if c.Tools.Genisoimage == "" {
    return fmt.Errorf("tools.genisoimage is required")
}

Remove the above lines.

3. config.example.yaml — remove tools: block

Remove lines:

tools:
  genisoimage: "/usr/bin/genisoimage"

4. internal/service/iso.go — replace genisoimage call

Current in GenerateISO() (line ~68-78):

isoPath := filepath.Join(s.cfg.ISO.TempDir, isoName)

exitCode, stdout, stderr, err := dicom.RunGenISOImage(ctx, s.cfg.Tools.Genisoimage, isoPath, tempDir)
if err != nil || exitCode != 0 {
    cleanup()
    os.Remove(isoPath)
    return nil, fmt.Errorf("genisoimage failed (exit %d): %s (stderr: %s)", exitCode, err, stderr)
}
_ = stdout

slog.Info("ISO created",
    "path", isoPath,
    "accession", accessionNumber,
)

Replace with:

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,
)

Same replacement in GenerateISOMultiple() — find the second dicom.RunGenISOImage call and replace identically.

Also add import: "mkiso-server/internal/isobuilder"

5. internal/handler/health.go — remove genisoimage from deps

Current (line ~19):

deps := []struct {
    Name   string `json:"name"`
    Path   string `json:"path"`
    Status string `json:"status"`
}{
    {"storescp", cfg.DCMTK.Storescp, ""},
    {"movescu", cfg.DCMTK.Movescu, ""},
    {"storescu", cfg.DCMTK.Storescu, ""},
    {"genisoimage", cfg.Tools.Genisoimage, ""},
}

Replace with (remove genisoimage entry, remove cfg.Tools reference):

deps := []struct {
    Name   string `json:"name"`
    Path   string `json:"path"`
    Status string `json:"status"`
}{
    {"storescp", cfg.DCMTK.Storescp, ""},
    {"movescu", cfg.DCMTK.Movescu, ""},
    {"storescu", cfg.DCMTK.Storescu, ""},
}

6. pkg/dicom/command.go — remove RunGenISOImage

Delete the entire RunGenISOImage function (lines ~150-180).

7. go.mod — after go get

Will add: github.com/diskfs/go-diskfs v1.9.3 plus its transitive deps.