201 lines
6.0 KiB
Go
201 lines
6.0 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// fsCloner is the pair of argv templates that stream one filesystem as an
|
|
// image: save reads the fs on dev and writes an image to stdout; restore
|
|
// reads that image from stdin and writes it onto dev. The image format is
|
|
// whatever the chosen standard tool uses (ntfsclone, partclone, e2image) —
|
|
// clonetool only moves the bytes between the two ends.
|
|
type fsCloner struct {
|
|
name string // tool family, for logs
|
|
save []string
|
|
restore []string
|
|
}
|
|
|
|
// fsCloneOpts is the JSON blob carried in CtrlMsg.Options for a fs-image
|
|
// partition clone so both ends build the identical argv.
|
|
type fsCloneOpts struct {
|
|
FSType string `json:"fsType"`
|
|
Tool string `json:"tool"`
|
|
SizeBytes int64 `json:"sizeBytes"`
|
|
// ShrinkToBytes > 0: shrink the SOURCE filesystem to this size in place
|
|
// before imaging it (so it fits a smaller target partition). This
|
|
// mutates the source and only runs when the operator passed
|
|
// --allow-shrink --yes.
|
|
ShrinkToBytes int64 `json:"shrinkToBytes,omitempty"`
|
|
}
|
|
|
|
func (o fsCloneOpts) encode() string { b, _ := json.Marshal(o); return string(b) }
|
|
func decodeFSCloneOpts(s string) (fsCloneOpts, error) {
|
|
var o fsCloneOpts
|
|
err := json.Unmarshal([]byte(s), &o)
|
|
return o, err
|
|
}
|
|
|
|
// pickCloner returns the fs-image cloner for (fsType, tool) or nil if the
|
|
// partition should be handled some other way. tool is the family name chosen
|
|
// by chooseCloneMethod ("ntfsclone", "partclone", "e2image").
|
|
func pickCloner(dev, fsType, tool string) *fsCloner {
|
|
switch tool {
|
|
case "ntfsclone":
|
|
return &fsCloner{
|
|
name: "ntfsclone",
|
|
save: []string{"ntfsclone", "--save-image", "--force", "--output", "-", dev},
|
|
restore: []string{"ntfsclone", "--restore-image", "--overwrite", dev, "-"},
|
|
}
|
|
case "e2image":
|
|
return &fsCloner{
|
|
name: "e2image",
|
|
// -ra: raw + skip unallocated; stream to/from stdio.
|
|
save: []string{"e2image", "-ra", "-p", dev, "-"},
|
|
restore: []string{"e2image", "-ra", "-p", "-", dev},
|
|
}
|
|
case "partclone":
|
|
pc := "partclone." + partcloneSuffix(fsType)
|
|
return &fsCloner{
|
|
name: pc,
|
|
save: []string{pc, "-c", "-s", dev, "-O", "-", "-L", "/dev/null"},
|
|
restore: []string{"partclone.restore", "-s", "-", "-O", dev, "-L", "/dev/null"},
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func partcloneSuffix(fsType string) string {
|
|
switch fsType {
|
|
case "ext2", "ext3", "ext4":
|
|
return "extfs"
|
|
case "vfat", "fat", "fat12", "fat16", "fat32", "msdos":
|
|
return "fat"
|
|
case "ntfs":
|
|
return "ntfs"
|
|
default:
|
|
return fsType // exfat, xfs, btrfs, f2fs, hfsplus, ...
|
|
}
|
|
}
|
|
|
|
// chooseCloneMethod decides how a partition's data is copied.
|
|
//
|
|
// "raw" -> the block-diff engine over an offset window (also the
|
|
// re-run-friendly path; used for swap, unknown fs, --raw)
|
|
// "fs-image" -> stream a filesystem image with a standard tool
|
|
// "file-level" -> mkfs on the target + rsync
|
|
func chooseCloneMethod(p Partition, tools map[string]bool, forceRaw, forceFile map[int]bool, fileAuto bool) (method, tool string) {
|
|
if forceRaw[p.Num] {
|
|
return "raw", ""
|
|
}
|
|
if forceFile[p.Num] {
|
|
return "file-level", ""
|
|
}
|
|
if p.isBIOSBoot() {
|
|
return "raw", "" // tiny, and holds GRUB core - always verbatim
|
|
}
|
|
switch p.FSType {
|
|
case "", "swap", "crypto_LUKS", "LVM2_member":
|
|
return "raw", ""
|
|
case "ntfs":
|
|
if tools["ntfsclone"] {
|
|
return "fs-image", "ntfsclone"
|
|
}
|
|
case "ext2", "ext3", "ext4":
|
|
if tools["partclone.extfs"] && tools["partclone.restore"] {
|
|
return "fs-image", "partclone"
|
|
}
|
|
if tools["e2image"] {
|
|
return "fs-image", "e2image"
|
|
}
|
|
case "vfat", "fat", "fat12", "fat16", "fat32", "msdos":
|
|
if tools["partclone.fat"] && tools["partclone.restore"] {
|
|
return "fs-image", "partclone"
|
|
}
|
|
case "exfat", "xfs", "btrfs", "f2fs", "hfsplus":
|
|
if tools["partclone."+partcloneSuffix(p.FSType)] && tools["partclone.restore"] {
|
|
return "fs-image", "partclone"
|
|
}
|
|
}
|
|
if fileAuto && p.FSType != "" && tools["rsync"] {
|
|
return "file-level", ""
|
|
}
|
|
return "raw", ""
|
|
}
|
|
|
|
// progressScanner reads a cloner's stderr line by line, pulls a percentage
|
|
// out of the tool's own progress chatter, and calls emit with an estimated
|
|
// byte count (fraction * sizeBytes). It swallows everything else.
|
|
var pctRe = regexp.MustCompile(`([0-9]{1,3}(?:\.[0-9]+)?)\s*%|([0-9]{1,3}(?:\.[0-9]+)?)\s*percent`)
|
|
|
|
func scanCloneProgress(r io.Reader, sizeBytes int64, emit func(bytesDone int64)) {
|
|
br := bufio.NewReader(r)
|
|
var buf []byte
|
|
flush := func() {
|
|
line := strings.TrimSpace(string(buf))
|
|
buf = buf[:0]
|
|
if line == "" {
|
|
return
|
|
}
|
|
m := pctRe.FindStringSubmatch(line)
|
|
if m == nil {
|
|
return
|
|
}
|
|
s := m[1]
|
|
if s == "" {
|
|
s = m[2]
|
|
}
|
|
if f, err := strconv.ParseFloat(s, 64); err == nil && f >= 0 && f <= 100 {
|
|
emit(int64(f / 100 * float64(sizeBytes)))
|
|
}
|
|
}
|
|
for {
|
|
c, err := br.ReadByte()
|
|
if err != nil {
|
|
flush()
|
|
return
|
|
}
|
|
if c == '\n' || c == '\r' {
|
|
flush()
|
|
continue
|
|
}
|
|
buf = append(buf, c)
|
|
if len(buf) > 4096 {
|
|
buf = buf[:0]
|
|
}
|
|
}
|
|
}
|
|
|
|
// preShrinkFS shrinks the filesystem on dev to (at least) toBytes in place.
|
|
// Used only on the source, only with --allow-shrink --yes. Best effort with a
|
|
// hard error if the resize tool fails.
|
|
func preShrinkFS(dev, fsType string, toBytes int64) error {
|
|
if toBytes <= 0 {
|
|
return nil
|
|
}
|
|
switch {
|
|
case isExtFS(fsType):
|
|
if out, err := runCmd("e2fsck", "-f", "-y", dev); err != nil {
|
|
return fmt.Errorf("e2fsck %s: %v: %s", dev, err, out)
|
|
}
|
|
kib := toBytes / 1024
|
|
if out, err := runCmd("resize2fs", dev, fmt.Sprintf("%dK", kib)); err != nil {
|
|
return fmt.Errorf("resize2fs %s %dK: %v: %s", dev, kib, err, out)
|
|
}
|
|
return nil
|
|
case fsType == "ntfs":
|
|
// ntfsresize prompts for confirmation on a real resize; feed it "y".
|
|
if out, err := runCmdStdin("y\n", "ntfsresize", "--force", "--size", strconv.FormatInt(toBytes, 10), dev); err != nil {
|
|
return fmt.Errorf("ntfsresize %s --size %d: %v: %s", dev, toBytes, err, out)
|
|
}
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("don't know how to shrink a %s filesystem", fsType)
|
|
}
|
|
}
|