1818package cmd
1919
2020import (
21- "bytes"
2221 "fmt"
2322 "io"
2423 "mime/multipart"
@@ -27,28 +26,20 @@ import (
2726 "path/filepath"
2827 "reflect"
2928 "strings"
29+ "time"
3030
31+ "github.com/apache/cloudstack-cloudmonkey/config"
3132 "github.com/briandowns/spinner"
3233)
3334
3435const (
35- uploadingMessage = "Uploading files, please wait..."
36+ uploadingMessage = "Uploading files, please wait..."
37+ progressCharCount = 24
3638)
3739
38- // PromptAndUploadFileIfNeeded prompts the user to provide file paths for upload and the API is getUploadParamsFor*
39- func PromptAndUploadFileIfNeeded (r * Request , api string , response map [string ]interface {}) {
40- if ! r .Config .HasShell {
41- return
42- }
43- apiName := strings .ToLower (api )
44- if apiName != "getuploadparamsforiso" &&
45- apiName != "getuploadparamsforvolume" &&
46- apiName != "getuploadparamsfotemplate" {
47- return
48- }
49- fmt .Print ("Enter path of the file(s) to upload (comma-separated): " )
50- var filePaths string
51- fmt .Scanln (& filePaths )
40+ // ValidateAndGetFileList parses a comma-separated string of file paths, trims them,
41+ // checks for existence, and returns a slice of valid file paths or an error if any are missing.
42+ func ValidateAndGetFileList (filePaths string ) ([]string , error ) {
5243 filePathsList := strings .FieldsFunc (filePaths , func (r rune ) bool { return r == ',' })
5344
5445 var missingFiles []string
@@ -65,13 +56,41 @@ func PromptAndUploadFileIfNeeded(r *Request, api string, response map[string]int
6556 }
6657 }
6758 if len (missingFiles ) > 0 {
68- fmt .Println ("File(s) do not exist or are not accessible:" , strings .Join (missingFiles , ", " ))
59+ return nil , fmt .Errorf ("file(s) do not exist or are not accessible: %s" , strings .Join (missingFiles , ", " ))
60+ }
61+ return validFiles , nil
62+ }
63+
64+ // PromptAndUploadFilesIfNeeded prompts the user to provide file paths for upload and the API is getUploadParamsFor*
65+ func PromptAndUploadFilesIfNeeded (r * Request , api string , response map [string ]interface {}) {
66+ if ! r .Config .HasShell {
67+ return
68+ }
69+ apiName := strings .ToLower (api )
70+ if ! config .IsFileUploadAPI (apiName ) {
71+ return
72+ }
73+ fmt .Print ("Enter path of the file(s) to upload (comma-separated), leave empty to skip: " )
74+ var filePaths string
75+ fmt .Scanln (& filePaths )
76+ if filePaths == "" {
77+ return
78+ }
79+ validFiles , err := ValidateAndGetFileList (filePaths )
80+ if err != nil {
81+ fmt .Println (err )
6982 return
7083 }
7184 if len (validFiles ) == 0 {
7285 fmt .Println ("No valid files to upload." )
7386 return
7487 }
88+ UploadFiles (r , api , response , validFiles )
89+ }
90+
91+ // UploadFiles uploads files to a remote server using parameters from the API response.
92+ // Shows progress for each file and reports any failures.
93+ func UploadFiles (r * Request , api string , response map [string ]interface {}, validFiles []string ) {
7594 paramsRaw , ok := response ["getuploadparams" ]
7695 if ! ok || reflect .TypeOf (paramsRaw ).Kind () != reflect .Map {
7796 fmt .Println ("Invalid response format for getuploadparams." )
@@ -85,7 +104,6 @@ func PromptAndUploadFileIfNeeded(r *Request, api string, response map[string]int
85104 return
86105 }
87106 }
88-
89107 postURL , _ := params ["postURL" ].(string )
90108 signature , _ := params ["signature" ].(string )
91109 expires , _ := params ["expires" ].(string )
@@ -96,7 +114,7 @@ func PromptAndUploadFileIfNeeded(r *Request, api string, response map[string]int
96114 errored := 0
97115 for i , filePath := range validFiles {
98116 spinner .Suffix = fmt .Sprintf (" uploading %d/%d %s..." , i + 1 , len (validFiles ), filepath .Base (filePath ))
99- if err := uploadFile (postURL , filePath , signature , expires , metadata , spinner ); err != nil {
117+ if err := uploadFile (i , len ( validFiles ), postURL , filePath , signature , expires , metadata , spinner ); err != nil {
100118 spinner .Stop ()
101119 fmt .Println ("Error uploading" , filePath , ":" , err )
102120 errored ++
@@ -112,79 +130,126 @@ func PromptAndUploadFileIfNeeded(r *Request, api string, response map[string]int
112130 }
113131}
114132
115- type progressReader struct {
116- file * os.File
117- total int64
118- read int64
119- updateSuffix func (percent int )
133+ // progressReader streams file data and updates progress as bytes are read.
134+ type progressBody struct {
135+ f * os.File
136+ read int64
137+ total int64
138+ update func (int )
120139}
121140
122- func (pr * progressReader ) Read (p []byte ) (int , error ) {
123- n , err := pr . file .Read (p )
141+ func (pb * progressBody ) Read (p []byte ) (int , error ) {
142+ n , err := pb . f .Read (p )
124143 if n > 0 {
125- pr .read += int64 (n )
126- percent := int (float64 (pr .read ) / float64 (pr .total ) * 100 )
127- pr . updateSuffix ( percent )
144+ pb .read += int64 (n )
145+ pct := int (float64 (pb .read ) * 100 / float64 (pb .total ))
146+ pb . update ( pct )
128147 }
129148 return n , err
130149}
150+ func (pb * progressBody ) Close () error { return pb .f .Close () }
151+
152+ func barArrow (pct int ) string {
153+ width := progressCharCount
154+ if pct < 0 {
155+ pct = 0
156+ }
157+ if pct > 100 {
158+ pct = 100
159+ }
160+ pos := (pct * width ) / 100
161+ // 100%: full bar, no head
162+ if pos >= width {
163+ return fmt .Sprintf ("[%s]" ,
164+ strings .Repeat ("=" , width ))
165+ }
166+ left := strings .Repeat ("=" , pos ) + ">"
167+ right := strings .Repeat (" " , width - pos - 1 )
168+
169+ return fmt .Sprintf ("[%s%s]" , left , right )
170+ }
131171
132- // uploadFile uploads a single file to the given postURL with the required headers .
133- func uploadFile (postURL , filePath , signature , expires , metadata string , spinner * spinner.Spinner ) error {
134- originalSuffix := spinner . Suffix
135- file , err := os .Open (filePath )
172+ // uploadFile streams a large file to the server with progress updates .
173+ func uploadFile (index , count int , postURL , filePath , signature , expires , metadata string , spn * spinner.Spinner ) error {
174+ fileName := filepath . Base ( filePath )
175+ in , err := os .Open (filePath )
136176 if err != nil {
137177 return err
138178 }
139- defer file .Close ()
140-
141- fileInfo , err := file .Stat ()
179+ defer in .Close ()
180+ _ , err = in .Stat ()
142181 if err != nil {
143182 return err
144183 }
145-
146- var body bytes.Buffer
147- writer := multipart .NewWriter (& body )
148- part , err := writer .CreateFormFile ("file" , filepath .Base (filePath ))
184+ tmp , err := os .CreateTemp ("" , "multipart-body-*.tmp" )
149185 if err != nil {
150186 return err
151187 }
152-
153- pr := & progressReader {
154- file : file ,
155- total : fileInfo .Size (),
156- updateSuffix : func (percent int ) {
157- spinner .Suffix = fmt .Sprintf (" %s (%d%%)" , originalSuffix , percent )
158- },
188+ defer func () {
189+ tmp .Close ()
190+ os .Remove (tmp .Name ())
191+ }()
192+ mw := multipart .NewWriter (tmp )
193+ part , err := mw .CreateFormFile ("file" , filepath .Base (filePath ))
194+ if err != nil {
195+ return err
159196 }
160- if _ , err := io .Copy (part , pr ); err != nil {
197+ if _ , err := io .Copy (part , in ); err != nil {
161198 return err
162199 }
163- writer .Close ()
164-
165- req , err := http .NewRequest ("POST" , postURL , & body )
200+ if err := mw .Close (); err != nil {
201+ return err
202+ }
203+ size , err := tmp .Seek (0 , io .SeekEnd )
166204 if err != nil {
167205 return err
168206 }
169- req .Header .Set ("Content-Type" , writer .FormDataContentType ())
207+ if _ , err := tmp .Seek (0 , io .SeekStart ); err != nil {
208+ return err
209+ }
210+ req , err := http .NewRequest ("POST" , postURL , nil )
211+ if err != nil {
212+ return err
213+ }
214+ req .Header .Set ("Content-Type" , mw .FormDataContentType ())
170215 req .Header .Set ("x-signature" , signature )
171216 req .Header .Set ("x-expires" , expires )
172217 req .Header .Set ("x-metadata" , metadata )
173-
174- client := & http.Client {}
218+ req .ContentLength = size
219+ pb := & progressBody {
220+ f : tmp ,
221+ total : size ,
222+ update : func (pct int ) {
223+ spn .Suffix = fmt .Sprintf (" [%d/%d] %s\t %s %d%%" , index + 1 , count , fileName , barArrow (pct ), pct )
224+ },
225+ }
226+ req .Body = pb
227+ req .GetBody = func () (io.ReadCloser , error ) {
228+ f , err := os .Open (tmp .Name ())
229+ if err != nil {
230+ return nil , err
231+ }
232+ return f , nil
233+ }
234+ client := & http.Client {
235+ Timeout : 24 * time .Hour ,
236+ Transport : & http.Transport {
237+ ExpectContinueTimeout : 0 ,
238+ },
239+ }
175240 resp , err := client .Do (req )
176241 if err != nil {
177242 return err
178243 }
179244 defer resp .Body .Close ()
180-
181245 if resp .StatusCode != http .StatusOK && resp .StatusCode != http .StatusCreated {
182- respBody , _ := io .ReadAll (resp .Body )
183- return fmt .Errorf ("upload failed: %s" , string (respBody ))
246+ b , _ := io .ReadAll (resp .Body )
247+ return fmt .Errorf ("[%d/%d] %s \t upload failed: %s" , index + 1 , count , fileName , string (b ))
184248 }
185- spinner .Stop ()
186- fmt .Println ("Upload successful for:" , filePath )
187- spinner .Suffix = fmt .Sprintf (" %s" , uploadingMessage )
188- spinner .Start ()
249+
250+ spn .Stop ()
251+ fmt .Printf ("[%d/%d] %s\t %s ✅\n " , index + 1 , count , fileName , barArrow (100 ))
252+ spn .Suffix = fmt .Sprintf (" %s" , uploadingMessage )
253+ spn .Start ()
189254 return nil
190255}
0 commit comments