Skip to content

Commit 09bd3af

Browse files
committed
feat: add progress indicator based on actually uploaded data
1 parent 90e121b commit 09bd3af

File tree

1 file changed

+105
-71
lines changed

1 file changed

+105
-71
lines changed

internal/cmd/beta/image/create/create.go

Lines changed: 105 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import (
44
"bufio"
55
"context"
66
"encoding/json"
7+
goerrors "errors"
78
"fmt"
9+
"io"
810
"net/http"
911
"os"
1012
"time"
@@ -23,9 +25,10 @@ import (
2325
)
2426

2527
const (
26-
nameFlag = "name"
27-
diskFormatFlag = "disk-format"
28-
localFilePathFlag = "local-file-path"
28+
nameFlag = "name"
29+
diskFormatFlag = "disk-format"
30+
localFilePathFlag = "local-file-path"
31+
noProgressIndicatorFlag = "no-progress"
2932

3033
bootMenuFlag = "boot-menu"
3134
cdromBusFlag = "cdrom-bus"
@@ -67,15 +70,16 @@ type imageConfig struct {
6770
type inputModel struct {
6871
*globalflags.GlobalFlagModel
6972

70-
Id *string
71-
Name string
72-
DiskFormat string
73-
LocalFilePath string
74-
Labels *map[string]string
75-
Config *imageConfig
76-
MinDiskSize *int64
77-
MinRam *int64
78-
Protected *bool
73+
Id *string
74+
Name string
75+
DiskFormat string
76+
LocalFilePath string
77+
Labels *map[string]string
78+
Config *imageConfig
79+
MinDiskSize *int64
80+
MinRam *int64
81+
Protected *bool
82+
NoProgressIndicator *bool
7983
}
8084

8185
func NewCmd(p *print.Printer) *cobra.Command {
@@ -138,7 +142,7 @@ func NewCmd(p *print.Printer) *cobra.Command {
138142
if !ok {
139143
return fmt.Errorf("create image: no upload URL has been provided")
140144
}
141-
if err := uploadAsync(ctx, p, file, *url); err != nil {
145+
if err := uploadAsync(ctx, p, model, file, *url); err != nil {
142146
return err
143147
}
144148

@@ -154,75 +158,104 @@ func NewCmd(p *print.Printer) *cobra.Command {
154158
return cmd
155159
}
156160

157-
func uploadAsync(ctx context.Context, p *print.Printer, file *os.File, url string) error {
158-
ticker := time.NewTicker(5 * time.Second)
159-
ch := uploadFile(ctx, p, file, url)
161+
func uploadAsync(ctx context.Context, p *print.Printer, model *inputModel, file *os.File, url string) error {
162+
stat, err := file.Stat()
163+
if err != nil {
164+
return fmt.Errorf("upload file: %w", err)
165+
}
160166

161-
start := time.Now()
162-
for {
163-
select {
164-
case <-ticker.C:
165-
p.Info("uploading for %s\n", time.Since(start))
166-
case err := <-ch:
167-
return err
168-
}
167+
var reader io.Reader
168+
if model.NoProgressIndicator != nil && *model.NoProgressIndicator {
169+
reader = file
170+
} else {
171+
var ch <-chan int
172+
reader, ch = newProgressReader(file)
173+
go func() {
174+
ticker := time.NewTicker(2 * time.Second)
175+
var uploaded int
176+
for {
177+
select {
178+
case <-ticker.C:
179+
p.Info("uploaded %3.1f%%\n", 100.0/float64(stat.Size())*float64(uploaded))
180+
case n := <-ch:
181+
if n >= 0 {
182+
uploaded += n
183+
}
184+
}
185+
}
186+
}()
187+
}
188+
189+
if err = uploadFile(ctx, p, reader, stat.Size(), url); err != nil {
190+
return fmt.Errorf("upload file: %w", err)
169191
}
192+
193+
return nil
170194
}
171195

172-
func uploadFile(ctx context.Context, p *print.Printer, file *os.File, url string) chan error {
173-
ch := make(chan error)
174-
go func() {
175-
defer close(ch)
176-
var filesize int64
177-
if stat, err := file.Stat(); err != nil {
178-
ch <- fmt.Errorf("create image: cannot read file size %q: %w", file.Name(), err)
179-
return
180-
} else {
181-
filesize = stat.Size()
182-
}
183-
p.Debug(print.DebugLevel, "uploading image to %s", url)
196+
var _ io.Reader = (*progressReader)(nil)
184197

185-
start := time.Now()
186-
// pass the file contents as stream, as they can get arbitrarily large. We do
187-
// _not_ want to load them into an internal buffer. The downside is, that we
188-
// have to set the content-length header manually
189-
uploadRequest, err := http.NewRequestWithContext(ctx, http.MethodPut, url, bufio.NewReader(file))
190-
if err != nil {
191-
ch <- fmt.Errorf("create image: cannot create request: %w", err)
192-
return
193-
}
194-
uploadRequest.Header.Add("Content-Type", "application/octet-stream")
195-
uploadRequest.ContentLength = filesize
198+
type progressReader struct {
199+
delegate io.Reader
200+
ch chan int
201+
}
196202

197-
uploadResponse, err := http.DefaultClient.Do(uploadRequest)
198-
if err != nil {
199-
ch <- fmt.Errorf("create image: error contacting server for upload: %w", err)
200-
return
201-
}
202-
defer func() {
203-
if inner := uploadResponse.Body.Close(); inner != nil {
204-
err = fmt.Errorf("error closing file: %w (%w)", inner, err)
205-
}
206-
}()
207-
if uploadResponse.StatusCode != http.StatusOK {
208-
ch <- fmt.Errorf("create image: server rejected image upload with %s", uploadResponse.Status)
209-
return
210-
}
211-
delay := time.Since(start)
212-
p.Debug(print.DebugLevel, "uploaded %d bytes in %v", filesize, delay)
203+
func newProgressReader(delegate io.Reader) (io.Reader, <-chan int) {
204+
ch := make(chan int)
205+
return &progressReader{
206+
delegate: delegate,
207+
ch: ch,
208+
}, ch
209+
}
210+
211+
// Read implements io.Reader.
212+
func (pr *progressReader) Read(p []byte) (int, error) {
213+
n, err := pr.delegate.Read(p)
214+
if goerrors.Is(err, io.EOF) && n <= 0 {
215+
close(pr.ch)
216+
} else {
217+
pr.ch <- n
218+
}
219+
return n, err
220+
}
221+
222+
func uploadFile(ctx context.Context, p *print.Printer, reader io.Reader, filesize int64, url string) error {
223+
p.Debug(print.DebugLevel, "uploading image to %s", url)
213224

214-
ch <- nil
215-
return
225+
start := time.Now()
226+
// pass the file contents as stream, as they can get arbitrarily large. We do
227+
// _not_ want to load them into an internal buffer. The downside is, that we
228+
// have to set the content-length header manually
229+
uploadRequest, err := http.NewRequestWithContext(ctx, http.MethodPut, url, bufio.NewReader(reader))
230+
if err != nil {
231+
return fmt.Errorf("create image: cannot create request: %w", err)
232+
}
233+
uploadRequest.Header.Add("Content-Type", "application/octet-stream")
234+
uploadRequest.ContentLength = filesize
216235

236+
uploadResponse, err := http.DefaultClient.Do(uploadRequest)
237+
if err != nil {
238+
return fmt.Errorf("create image: error contacting server for upload: %w", err)
239+
}
240+
defer func() {
241+
if inner := uploadResponse.Body.Close(); inner != nil {
242+
err = fmt.Errorf("error closing file: %w (%w)", inner, err)
243+
}
217244
}()
245+
if uploadResponse.StatusCode != http.StatusOK {
246+
return fmt.Errorf("create image: server rejected image upload with %s", uploadResponse.Status)
247+
}
248+
delay := time.Since(start)
249+
p.Debug(print.DebugLevel, "uploaded %d bytes in %v", filesize, delay)
218250

219-
return ch
251+
return nil
220252
}
221253

222254
func configureFlags(cmd *cobra.Command) {
223255
cmd.Flags().String(nameFlag, "", "The name of the image.")
224256
cmd.Flags().String(diskFormatFlag, "", "The disk format of the image. ")
225257
cmd.Flags().String(localFilePathFlag, "", "The path to the local disk image file.")
258+
cmd.Flags().Bool(noProgressIndicatorFlag, false, "Show no progress indicator for upload.")
226259

227260
cmd.Flags().Bool(bootMenuFlag, false, "Enables the BIOS bootmenu.")
228261
cmd.Flags().String(cdromBusFlag, "", "Sets CDROM bus controller type.")
@@ -257,11 +290,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
257290
name := flags.FlagToStringValue(p, cmd, nameFlag)
258291

259292
model := inputModel{
260-
GlobalFlagModel: globalFlags,
261-
Name: name,
262-
DiskFormat: flags.FlagToStringValue(p, cmd, diskFormatFlag),
263-
LocalFilePath: flags.FlagToStringValue(p, cmd, localFilePathFlag),
264-
Labels: flags.FlagToStringToStringPointer(p, cmd, labelsFlag),
293+
GlobalFlagModel: globalFlags,
294+
Name: name,
295+
DiskFormat: flags.FlagToStringValue(p, cmd, diskFormatFlag),
296+
LocalFilePath: flags.FlagToStringValue(p, cmd, localFilePathFlag),
297+
Labels: flags.FlagToStringToStringPointer(p, cmd, labelsFlag),
298+
NoProgressIndicator: flags.FlagToBoolPointer(p, cmd, noProgressIndicatorFlag),
265299
Config: &imageConfig{
266300
BootMenu: flags.FlagToBoolPointer(p, cmd, bootMenuFlag),
267301
CdromBus: flags.FlagToStringPointer(p, cmd, cdromBusFlag),

0 commit comments

Comments
 (0)