# Detail: genisoimage → go-diskfs

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

```go
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):**
```go
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):**
```go
if c.Tools.Genisoimage == "" {
    return fmt.Errorf("tools.genisoimage is required")
}
```

**Remove the above lines.**

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

Remove lines:
```yaml
tools:
  genisoimage: "/usr/bin/genisoimage"
```

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

**Current in GenerateISO() (line ~68-78):**
```go
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:**
```go
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):**
```go
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):**
```go
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.
