feat: base go mkiso
This commit is contained in:
131
internal/isobuilder/builder.go
Normal file
131
internal/isobuilder/builder.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user