Skip to content

Commit 0473ecd

Browse files
authored
Merge pull request #38 from nirs/block-status
Add Image.Extent() interface
2 parents a04a2a1 + 82009e9 commit 0473ecd

File tree

11 files changed

+955
-40
lines changed

11 files changed

+955
-40
lines changed

cmd/go-qcow2reader-example/convert.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"os"
88

9+
"github.com/cheggaaa/pb/v3"
910
"github.com/lima-vm/go-qcow2reader"
1011
"github.com/lima-vm/go-qcow2reader/convert"
1112
"github.com/lima-vm/go-qcow2reader/log"
@@ -76,7 +77,12 @@ func cmdConvert(args []string) error {
7677
if err != nil {
7778
return err
7879
}
79-
if err := c.Convert(t, img, img.Size()); err != nil {
80+
81+
bar := newProgressBar(img.Size())
82+
bar.Start()
83+
defer bar.Finish()
84+
85+
if err := c.Convert(t, img, img.Size(), bar); err != nil {
8086
return err
8187
}
8288

@@ -86,3 +92,18 @@ func cmdConvert(args []string) error {
8692

8793
return t.Close()
8894
}
95+
96+
// progressBar adapts pb.ProgressBar to the Updater interface.
97+
type progressBar struct {
98+
*pb.ProgressBar
99+
}
100+
101+
func newProgressBar(size int64) *progressBar {
102+
b := &progressBar{pb.New64(size)}
103+
b.Set(pb.Bytes, true)
104+
return b
105+
}
106+
107+
func (b *progressBar) Update(n int64) {
108+
b.ProgressBar.Add64(n)
109+
}

cmd/go-qcow2reader-example/go.mod

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,19 @@ module github.com/lima-vm/go-qcow2reader/cmd/go-qcow2reader-example
33
go 1.22
44

55
require (
6+
github.com/cheggaaa/pb/v3 v3.1.5
67
github.com/klauspost/compress v1.16.5
78
github.com/lima-vm/go-qcow2reader v0.0.0-00010101000000-000000000000
89
)
910

11+
require (
12+
github.com/VividCortex/ewma v1.2.0 // indirect
13+
github.com/fatih/color v1.15.0 // indirect
14+
github.com/mattn/go-colorable v0.1.13 // indirect
15+
github.com/mattn/go-isatty v0.0.19 // indirect
16+
github.com/mattn/go-runewidth v0.0.15 // indirect
17+
github.com/rivo/uniseg v0.2.0 // indirect
18+
golang.org/x/sys v0.6.0 // indirect
19+
)
20+
1021
replace github.com/lima-vm/go-qcow2reader => ../../

cmd/go-qcow2reader-example/go.sum

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,20 @@
1+
github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow=
2+
github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4=
3+
github.com/cheggaaa/pb/v3 v3.1.5 h1:QuuUzeM2WsAqG2gMqtzaWithDJv0i+i6UlnwSCI4QLk=
4+
github.com/cheggaaa/pb/v3 v3.1.5/go.mod h1:CrxkeghYTXi1lQBEI7jSn+3svI3cuc19haAj6jM60XI=
5+
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
6+
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
17
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
28
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
9+
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
10+
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
11+
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
12+
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
13+
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
14+
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
15+
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
16+
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
17+
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
18+
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
19+
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
20+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

cmd/go-qcow2reader-example/main.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ func usage() {
4141
Available commands:
4242
info show image information
4343
read read image data and print to stdout
44-
convert convert image to raw format
44+
convert convert image to raw format
45+
map print image extents
4546
`
4647
fmt.Fprintf(os.Stderr, usage, os.Args[0])
4748
os.Exit(1)
@@ -71,6 +72,8 @@ func main() {
7172
err = cmdRead(args)
7273
case "convert":
7374
err = cmdConvert(args)
75+
case "map":
76+
err = cmdMap(args)
7477
default:
7578
usage()
7679
}

cmd/go-qcow2reader-example/map.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"encoding/json"
6+
"errors"
7+
"flag"
8+
"fmt"
9+
"os"
10+
11+
"github.com/lima-vm/go-qcow2reader"
12+
"github.com/lima-vm/go-qcow2reader/log"
13+
)
14+
15+
func cmdMap(args []string) error {
16+
var (
17+
// Required
18+
filename string
19+
20+
// Options
21+
debug bool
22+
)
23+
24+
fs := flag.NewFlagSet("map", flag.ExitOnError)
25+
fs.Usage = func() {
26+
fmt.Fprintf(fs.Output(), "Usage: %s map [OPTIONS...] FILE\n", os.Args[0])
27+
flag.PrintDefaults()
28+
}
29+
fs.BoolVar(&debug, "debug", false, "enable printing debug messages")
30+
if err := fs.Parse(args); err != nil {
31+
return err
32+
}
33+
34+
if debug {
35+
log.SetDebugFunc(logDebug)
36+
}
37+
38+
switch len(fs.Args()) {
39+
case 0:
40+
return errors.New("no file was specified")
41+
case 1:
42+
filename = fs.Arg(0)
43+
default:
44+
return errors.New("too many files were specified")
45+
}
46+
47+
f, err := os.Open(filename)
48+
if err != nil {
49+
return err
50+
}
51+
defer f.Close()
52+
53+
img, err := qcow2reader.Open(f)
54+
if err != nil {
55+
return err
56+
}
57+
defer img.Close()
58+
59+
writer := bufio.NewWriter(os.Stdout)
60+
encoder := json.NewEncoder(writer)
61+
62+
var start int64
63+
end := img.Size()
64+
for start < end {
65+
extent, err := img.Extent(start, end-start)
66+
if err != nil {
67+
return err
68+
}
69+
encoder.Encode(extent)
70+
start += extent.Length
71+
}
72+
return writer.Flush()
73+
}

convert/convert.go

Lines changed: 79 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,29 @@ import (
66
"fmt"
77
"io"
88
"sync"
9+
10+
"github.com/lima-vm/go-qcow2reader/image"
911
)
1012

13+
// The size of the buffer used to read data from non-zero extents of the image.
14+
// For best performance, the size should be aligned to the image cluster size or
15+
// the file system block size.
1116
const BufferSize = 1024 * 1024
1217

13-
// Smaller value may increase the overhead of synchornizing multiple works.
18+
// To maxmimze performance we use multiple goroutines to read data from the
19+
// source image, decompress data, and write data to the target image. To
20+
// schedule the work to multiple goroutines, the image is split to multiple
21+
// segments, each processed by a single worker goroutine.
22+
//
23+
// Smaller value may increase the overhead of synchornizing multiple workers.
1424
// Larger value may be less efficient for smaller images. The default value
15-
// gives good results for the lima default Ubuntu image.
25+
// gives good results for the lima default Ubuntu image. Must be aligned to
26+
// BufferSize.
1627
const SegmentSize = 32 * BufferSize
1728

1829
// For best I/O throughput we want to have enough in-flight requests, regardless
1930
// of number of cores. For best decompression we want to use one worker per
20-
// core, but too many workers is less effective. The default value gives good
31+
// core, but too many workers are less effective. The default value gives good
2132
// results with lima default Ubuntu image.
2233
const Workers = 8
2334

@@ -67,6 +78,14 @@ func (o *Options) Validate() error {
6778
return nil
6879
}
6980

81+
// Updater is an interface for tracking conversion progress.
82+
type Updater interface {
83+
// Called from multiple goroutines after a byte range of length was converted.
84+
// If the conversion is successfu, the total number of bytes will be the image
85+
// virtual size.
86+
Update(n int64)
87+
}
88+
7089
type Converter struct {
7190
// Read only after starting.
7291
size int64
@@ -129,10 +148,13 @@ func (c *Converter) reset(size int64) {
129148
c.offset = 0
130149
}
131150

132-
// Convert copy size bytes from io.ReaderAt to io.WriterAt. Unallocated areas or
133-
// areas full of zeros in the source are keep unallocated in the destination.
134-
// The destination must be new empty or full of zeroes.
135-
func (c *Converter) Convert(wa io.WriterAt, ra io.ReaderAt, size int64) error {
151+
// Convert copy size bytes from image to io.WriterAt. Unallocated extents in the
152+
// source image or read data which is all zeros are converted to unallocated
153+
// byte range in the target image. The target image must be new empty file or a
154+
// file full of zeroes. To get a sparse target image, the image must be a new
155+
// empty file, since Convert does not punch holes for zero ranges even if the
156+
// underlying file system supports hole punching.
157+
func (c *Converter) Convert(wa io.WriterAt, img image.Image, size int64, progress Updater) error {
136158
c.reset(size)
137159

138160
zero := make([]byte, c.bufferSize)
@@ -151,40 +173,64 @@ func (c *Converter) Convert(wa io.WriterAt, ra io.ReaderAt, size int64) error {
151173
}
152174

153175
for start < end {
154-
// The last read may be shorter.
155-
n := len(buf)
156-
if end-start < int64(len(buf)) {
157-
n = int(end - start)
176+
// Get next extent in this segment.
177+
extent, err := img.Extent(start, end-start)
178+
if err != nil {
179+
c.setError(err)
180+
return
181+
}
182+
if extent.Zero {
183+
start += extent.Length
184+
if progress != nil {
185+
progress.Update(extent.Length)
186+
}
187+
continue
158188
}
159189

160-
// Read more data.
161-
nr, err := ra.ReadAt(buf[:n], start)
162-
if err != nil {
163-
if !errors.Is(err, io.EOF) {
164-
c.setError(err)
165-
return
190+
// Consume data from this extent.
191+
for extent.Length > 0 {
192+
// The last read may be shorter.
193+
n := len(buf)
194+
if extent.Length < int64(len(buf)) {
195+
n = int(extent.Length)
166196
}
167197

168-
// EOF for the last read of the last segment is expected, but since we
169-
// read exactly size bytes, we should never get a zero read.
170-
if nr == 0 {
171-
c.setError(errors.New("unexpected EOF"))
172-
return
198+
// Read more data.
199+
nr, err := img.ReadAt(buf[:n], start)
200+
if err != nil {
201+
if !errors.Is(err, io.EOF) {
202+
c.setError(err)
203+
return
204+
}
205+
206+
// EOF for the last read of the last segment is expected, but since we
207+
// read exactly size bytes, we should never get a zero read.
208+
if nr == 0 {
209+
c.setError(errors.New("unexpected EOF"))
210+
return
211+
}
173212
}
174-
}
175213

176-
// If the data is all zeros we skip it to create a hole. Otherwise
177-
// write the data.
178-
if !bytes.Equal(buf[:nr], zero[:nr]) {
179-
if nw, err := wa.WriteAt(buf[:nr], start); err != nil {
180-
c.setError(err)
181-
return
182-
} else if nw != nr {
183-
c.setError(fmt.Errorf("read %d, but wrote %d bytes", nr, nw))
184-
return
214+
// If the data is all zeros we skip it to create a hole. Otherwise
215+
// write the data.
216+
if !bytes.Equal(buf[:nr], zero[:nr]) {
217+
if nw, err := wa.WriteAt(buf[:nr], start); err != nil {
218+
c.setError(err)
219+
return
220+
} else if nw != nr {
221+
c.setError(fmt.Errorf("read %d, but wrote %d bytes", nr, nw))
222+
return
223+
}
185224
}
225+
226+
if progress != nil {
227+
progress.Update(int64(nr))
228+
}
229+
230+
extent.Length -= int64(nr)
231+
extent.Start += int64(nr)
232+
start += int64(nr)
186233
}
187-
start += int64(nr)
188234
}
189235
}
190236
}()

image/image.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,28 @@ import (
88
// Type must be a "Backing file format name string" that appears in QCOW2.
99
type Type string
1010

11+
// Extent describes a byte range in the image with the same allocation,
12+
// compression, or zero status. Extents are aligned to the underlying file
13+
// system block size (e.g. 4k), or the image format cluster size (e.g. 64k). One
14+
// extent can describe one or more file system blocks or image clusters.
15+
type Extent struct {
16+
// Offset from start of the image in bytes.
17+
Start int64 `json:"start"`
18+
// Length of this extent in bytes.
19+
Length int64 `json:"length"`
20+
// Set if this extent is allocated.
21+
Allocated bool `json:"allocated"`
22+
// Set if this extent is read as zeros.
23+
Zero bool `json:"zero"`
24+
// Set if this extent is compressed.
25+
Compressed bool `json:"compressed"`
26+
}
27+
1128
// Image implements [io.ReaderAt] and [io.Closer].
1229
type Image interface {
1330
io.ReaderAt
1431
io.Closer
32+
Extent(start, length int64) (Extent, error)
1533
Type() Type
1634
Size() int64 // -1 if unknown
1735
Readable() error

0 commit comments

Comments
 (0)