feat: base go mkiso

This commit is contained in:
2026-06-05 08:11:44 +07:00
commit 983667a76a
63 changed files with 5322 additions and 0 deletions

View File

@@ -0,0 +1,131 @@
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 extensions
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
}

View File

@@ -0,0 +1,79 @@
package isobuilder
import (
"os"
"os/exec"
"path/filepath"
"testing"
)
func TestBuildFromDirectory(t *testing.T) {
srcDir := "/tmp/build_iso/xcdrom"
isoPath := "/tmp/dicom_purego_test.iso"
// Check if source exists (might need to be created first)
if _, err := os.Stat(srcDir); os.IsNotExist(err) {
t.Skipf("source dir %s does not exist, skipping", srcDir)
}
// Clean up any previous test ISO
os.Remove(isoPath)
// Build ISO using pure Go
err := BuildFromDirectory(srcDir, isoPath, "DICOM")
if err != nil {
t.Fatalf("BuildFromDirectory failed: %v", err)
}
defer os.Remove(isoPath)
// Verify ISO file exists and has reasonable size
info, err := os.Stat(isoPath)
if err != nil {
t.Fatalf("cannot stat ISO: %v", err)
}
if info.Size() == 0 {
t.Fatal("ISO is empty")
}
t.Logf("ISO created: %s (%.2f MB)", isoPath, float64(info.Size())/1024/1024)
// Verify it's a valid ISO 9660 using file command
cmd := exec.Command("file", isoPath)
output, _ := cmd.Output()
t.Logf("file type: %s", string(output))
// Verify contents using isoinfo
cmd = exec.Command("isoinfo", "-l", "-i", isoPath)
output, _ = cmd.Output()
t.Logf("ISO listing:\n%s", string(output))
}
func TestDirSize(t *testing.T) {
// Create a temp dir with known file sizes
tmpDir := t.TempDir()
// Create a 1KB file
f1 := filepath.Join(tmpDir, "file1.bin")
os.WriteFile(f1, make([]byte, 1024), 0644)
// Create a subdirectory with a 2KB file
subDir := filepath.Join(tmpDir, "subdir")
os.Mkdir(subDir, 0755)
f2 := filepath.Join(subDir, "file2.bin")
os.WriteFile(f2, make([]byte, 2048), 0644)
size, err := dirSize(tmpDir)
if err != nil {
t.Fatalf("dirSize failed: %v", err)
}
// Expected: (1024 + 2048) = 3072 + 10% = 3379,
// minimum 10MB = 10485760, round up to 2048: 10485760
// Since 3072 < 10MB, should be 10485760 (already aligned)
if size < 10*1024*1024 {
t.Fatalf("expected minimum 10MB, got %d", size)
}
if size%2048 != 0 {
t.Fatalf("expected 2048-aligned, got %d", size)
}
t.Logf("dirSize(%s) = %d bytes (%.2f MB)", tmpDir, size, float64(size)/1024/1024)
}