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
2527const (
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 {
6770type 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
8185func 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
222254func 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