clonetool/plan.go
2026-09-06 20:15:54 +02:00

216 lines
6.5 KiB
Go

package main
import (
"fmt"
"strings"
)
// plannedPart is one partition's copy plan: where it lives on each side (in
// bytes) and how its data moves.
type plannedPart struct {
Num int
SrcStartB, SrcSizeB int64
DstStartB, DstSizeB int64
Method string // "raw" | "fs-image" | "file-level"
Tool string // fs-image tool family
FSType string
ShrinkToB int64 // >0: shrink the fs to this many bytes before imaging
}
type targetPlan struct {
Script string // sfdisk restore script ("" when the source has no table / Windows-native)
IsFile bool
ImageSize int64 // file target: truncate to this
Parts []plannedPart
BootRegions []Region
RootPart int
ESPPart int
UEFI bool
Notes []string
}
// bootOpts is the JSON blob passed to reinstall_boot.
type bootOpts struct {
RootPart int `json:"rootPart"` // 1-based partition number holding "/"
ESPPart int `json:"espPart"` // 1-based ESP partition number, 0 if BIOS-only
UEFI bool `json:"uefi"`
DiskPath string `json:"diskPath"` // whole-disk path grub-install targets
}
type planOpts struct {
Parts map[int]bool // empty = all
Raw map[int]bool
FileLevel map[int]bool
FileAuto bool
AllowShrink bool
NewIDs bool
ImageSize int64 // explicit file-target size, 0 = auto
}
const gptTailSectors = 33 // secondary GPT header + entries
// planTargetLayout turns a probed source layout + the target's nature/size
// into a concrete copy plan, applying the sizing rules:
//
// - device target: never grow; if everything fits, keep the layout and
// leave trailing space untouched; if the last partition(s) overflow,
// shrink them (filesystem + partition) from the last inward, or fail.
// - file target: size the image to hold the layout (or ImageSize).
func planTargetLayout(src *DiskLayout, isFile bool, destSize int64, o planOpts) (*targetPlan, error) {
sector := src.LogicalSector
if sector <= 0 {
sector = 512
}
parts := sortedParts(src.Partitions)
var sel []Partition
for _, p := range parts {
if len(o.Parts) == 0 || o.Parts[p.Num] {
sel = append(sel, p)
}
}
if len(sel) == 0 && src.Scheme != "raw" {
return nil, fmt.Errorf("no partitions selected")
}
plan := &targetPlan{IsFile: isFile, BootRegions: append([]Region(nil), src.BootRegions...)}
// With --new-ids on an MBR disk, keep the raw bootstrap copy clear of the
// 4-byte disk signature at offset 0x1B8 so sfdisk's freshly generated one
// survives (GPT regenerates its GUID in the header, which we never
// raw-copy, so no clamp is needed there).
if o.NewIDs && src.Scheme == "mbr" {
for i := range plan.BootRegions {
r := &plan.BootRegions[i]
if r.Offset == 0 && r.Length > 0x1B8 {
r.Length = 0x1B8
}
}
}
// Bytes the layout needs on disk = end of the last selected partition
// (+ secondary GPT for GPT disks).
lastEndB := int64(0)
for _, p := range sel {
if e := (p.Start + p.Size) * sector; e > lastEndB {
lastEndB = e
}
}
neededB := lastEndB
if src.Scheme == "gpt" {
neededB += gptTailSectors * sector
}
if src.Scheme == "raw" {
neededB = src.DiskSize
}
if isFile {
plan.ImageSize = o.ImageSize
if plan.ImageSize == 0 {
plan.ImageSize = roundUp(neededB+(1<<20), 1<<20)
}
destSize = plan.ImageSize
}
if destSize <= 0 {
return nil, fmt.Errorf("could not determine target size")
}
// --- shrink pass ----------------------------------------------------
sizeOverride := map[int]int64{}
if destSize < neededB {
short := neededB - destSize
for i := len(sel) - 1; i >= 0 && short > 0; i-- {
p := &sel[i]
curB := p.Size * sector
minB := p.FSMinBytes
shrinkable := minB > 0 && (isExtFS(p.FSType) || p.FSType == "ntfs")
if !shrinkable {
continue
}
minB = roundUp(minB+(16<<20), sector) // 16 MiB slack
if minB >= curB {
continue
}
reduce := curB - minB
if reduce > short {
reduce = roundUp(short, sector)
}
newB := curB - reduce
sizeOverride[p.Num] = newB / sector
plan.Notes = append(plan.Notes,
fmt.Sprintf("partition p%d (%s) shrinks %s -> %s to fit the target",
p.Num, p.FSType, humanBytes(curB), humanBytes(newB)))
short -= reduce
}
if short > 0 {
return nil, fmt.Errorf(
"target (%s) is smaller than the source layout needs (%s); the trailing partition(s) "+
"cannot shrink to make up the missing %s; use a larger target",
humanBytes(destSize), humanBytes(neededB), humanBytes(short))
}
if !o.AllowShrink {
var names []string
for n := range sizeOverride {
names = append(names, fmt.Sprintf("p%d", n))
}
return nil, fmt.Errorf(
"target is smaller than the source; partition(s) %s would have to be shrunk. "+
"Re-run with --allow-shrink (this resizes the SOURCE filesystem in place before imaging) "+
"or use --no-shrink off / a larger target",
strings.Join(names, ", "))
}
}
// --- partition-table script --------------------------------------
if src.Scheme != "raw" && strings.TrimSpace(src.SfdiskDump) != "" {
plan.Script = src.sfdiskRestoreScript(o.NewIDs, sizeOverride)
}
// --- per-partition method --------------------------------------
for _, p := range sel {
pp := plannedPart{
Num: p.Num,
SrcStartB: p.Start * sector,
SrcSizeB: p.Size * sector,
DstStartB: p.Start * sector,
DstSizeB: p.Size * sector,
FSType: p.FSType,
}
if ov, ok := sizeOverride[p.Num]; ok {
pp.DstSizeB = ov * sector
pp.ShrinkToB = ov * sector
}
method, tool := chooseCloneMethod(p, src.Tools, o.Raw, o.FileLevel, o.FileAuto)
pp.Method, pp.Tool = method, tool
if pp.ShrinkToB > 0 && method != "fs-image" {
return nil, fmt.Errorf(
"partition p%d (%s) must shrink to fit but no image cloner is available for it; "+
"install ntfsclone / partclone or use a larger target", p.Num, orDash(p.FSType))
}
plan.Parts = append(plan.Parts, pp)
}
// --- boot roles ------------------------------------------------
plan.RootPart, plan.ESPPart = pickBootParts(sel)
plan.UEFI = plan.ESPPart != 0
return plan, nil
}
func isExtFS(t string) bool { return t == "ext2" || t == "ext3" || t == "ext4" }
// pickBootParts guesses the "/" partition (largest Linux fs) and the ESP.
func pickBootParts(sel []Partition) (root, esp int) {
var bestSize int64
for _, p := range sel {
if p.isESP() && esp == 0 {
esp = p.Num
}
switch p.FSType {
case "ext2", "ext3", "ext4", "btrfs", "xfs":
if sz := p.Size; sz > bestSize {
bestSize, root = sz, p.Num
}
}
}
return root, esp
}