-
Notifications
You must be signed in to change notification settings - Fork 559
/
nativeimgutil.go
175 lines (163 loc) · 4.49 KB
/
nativeimgutil.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
// Package nativeimgutil provides image utilities that do not depend on `qemu-img` binary.
package nativeimgutil
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"github.com/containerd/continuity/fs"
"github.com/docker/go-units"
"github.com/lima-vm/go-qcow2reader"
"github.com/lima-vm/go-qcow2reader/image/qcow2"
"github.com/lima-vm/go-qcow2reader/image/raw"
"github.com/lima-vm/lima/pkg/osutil"
"github.com/lima-vm/lima/pkg/progressbar"
"github.com/sirupsen/logrus"
)
// ConvertToRaw converts a source disk into a raw disk.
// source and dest may be same.
// ConvertToRaw is a NOP if source == dest, and no resizing is needed.
func ConvertToRaw(source, dest string, size *int64, allowSourceWithBackingFile bool) error {
srcF, err := os.Open(source)
if err != nil {
return err
}
defer srcF.Close()
srcImg, err := qcow2reader.Open(srcF)
if err != nil {
return fmt.Errorf("failed to detect the format of %q: %w", source, err)
}
if size != nil && *size < srcImg.Size() {
return fmt.Errorf("specified size %d is smaller than the original image size (%d) of %q", *size, srcImg.Size(), source)
}
logrus.Infof("Converting %q (%s) to a raw disk %q", source, srcImg.Type(), dest)
switch t := srcImg.Type(); t {
case raw.Type:
if err = srcF.Close(); err != nil {
return err
}
return convertRawToRaw(source, dest, size)
case qcow2.Type:
if !allowSourceWithBackingFile {
q, ok := srcImg.(*qcow2.Qcow2)
if !ok {
return fmt.Errorf("unexpected qcow2 image %T", srcImg)
}
if q.BackingFile != "" {
return fmt.Errorf("qcow2 image %q has an unexpected backing file: %q", source, q.BackingFile)
}
}
default:
logrus.Warnf("image %q has an unexpected format: %q", source, t)
}
if err = srcImg.Readable(); err != nil {
return fmt.Errorf("image %q is not readable: %w", source, err)
}
// Create a tmp file because source and dest can be same.
destTmpF, err := os.CreateTemp(filepath.Dir(dest), filepath.Base(dest)+".lima-*.tmp")
if err != nil {
return err
}
destTmp := destTmpF.Name()
defer os.RemoveAll(destTmp)
defer destTmpF.Close()
// Copy
srcImgR := io.NewSectionReader(srcImg, 0, srcImg.Size())
bar, err := progressbar.New(srcImg.Size())
if err != nil {
return err
}
const bufSize = 1024 * 1024
bar.Start()
copied, err := copySparse(destTmpF, bar.NewProxyReader(srcImgR), bufSize)
bar.Finish()
if err != nil {
return fmt.Errorf("failed to call copySparse(), bufSize=%d, copied=%d: %w", bufSize, copied, err)
}
// Resize
if size != nil {
logrus.Infof("Expanding to %s", units.BytesSize(float64(*size)))
if err = MakeSparse(destTmpF, *size); err != nil {
return err
}
}
if err = destTmpF.Close(); err != nil {
return err
}
// Rename destTmp into dest
if err = os.RemoveAll(dest); err != nil {
return err
}
return os.Rename(destTmp, dest)
}
func convertRawToRaw(source, dest string, size *int64) error {
if source != dest {
// continuity attempts clonefile
if err := fs.CopyFile(dest, source); err != nil {
return fmt.Errorf("failed to copy %q into %q: %w", source, dest, err)
}
}
if size != nil {
logrus.Infof("Expanding to %s", units.BytesSize(float64(*size)))
destF, err := os.OpenFile(dest, os.O_RDWR, 0o644)
if err != nil {
return err
}
if err = MakeSparse(destF, *size); err != nil {
_ = destF.Close()
return err
}
return destF.Close()
}
return nil
}
func copySparse(w *os.File, r io.Reader, bufSize int64) (int64, error) {
var (
n int64
eof, hasWrites bool
)
zeroBuf := make([]byte, bufSize)
buf := make([]byte, bufSize)
for !eof {
rN, rErr := r.Read(buf)
if rErr != nil {
eof = errors.Is(rErr, io.EOF)
if !eof {
return n, fmt.Errorf("failed to read: %w", rErr)
}
}
// TODO: qcow2reader should have a method to notify whether buf is zero
if bytes.Equal(buf, zeroBuf) {
if _, sErr := w.Seek(int64(rN), io.SeekCurrent); sErr != nil {
return n, fmt.Errorf("failed seek: %w", sErr)
}
// no need to ftruncate here
n += int64(rN)
} else {
hasWrites = true
wN, wErr := w.Write(buf)
if wN > 0 {
n += int64(wN)
}
if wErr != nil {
return n, fmt.Errorf("failed to read: %w", wErr)
}
if wN != rN {
return n, fmt.Errorf("read %d, but wrote %d bytes", rN, wN)
}
}
}
// Ftruncate must be run if the file contains only zeros
if !hasWrites {
return n, MakeSparse(w, n)
}
return n, nil
}
func MakeSparse(f *os.File, n int64) error {
if _, err := f.Seek(n, io.SeekStart); err != nil {
return err
}
return osutil.Ftruncate(int(f.Fd()), n)
}