From d6c4fccfbf92e13f4b0db5a70270769f3c29698a Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 7 Aug 2025 15:51:58 +0530 Subject: [PATCH 1/3] feature: allow file upload for getuploadparams* apis Fixes #144 Signed-off-by: Abhishek Kumar --- cmd/api.go | 116 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 1 deletion(-) diff --git a/cmd/api.go b/cmd/api.go index 872a328..479a92b 100644 --- a/cmd/api.go +++ b/cmd/api.go @@ -18,8 +18,15 @@ package cmd import ( + "bytes" "errors" "fmt" + "io" + "mime/multipart" + "net/http" + "os" + "path/filepath" + "reflect" "strings" ) @@ -111,9 +118,116 @@ func init() { if len(response) > 0 { printResult(r.Config.Core.Output, response, filterKeys, excludeKeys) + if r.Config.HasShell { + apiName := strings.ToLower(api.Name) + if apiName == "getuploadparamsforiso" || + apiName == "getuploadparamsforvolume" || + apiName == "getuploadparamsfotemplate" { + promptForFileUpload(r, apiName, response) + } + } } - return nil }, } } + +func promptForFileUpload(r *Request, api string, response map[string]interface{}) { + fmt.Print("Enter path of the file(s) to upload (comma-separated): ") + var filePaths string + fmt.Scanln(&filePaths) + filePathsList := strings.FieldsFunc(filePaths, func(r rune) bool { return r == ',' }) + + var missingFiles []string + var validFiles []string + for _, filePath := range filePathsList { + filePath = strings.TrimSpace(filePath) + if filePath == "" { + continue + } + if _, err := os.Stat(filePath); os.IsNotExist(err) { + missingFiles = append(missingFiles, filePath) + } else { + validFiles = append(validFiles, filePath) + } + } + if len(missingFiles) > 0 { + fmt.Println("File(s) do not exist or are not accessible:", strings.Join(missingFiles, ", ")) + return + } + if len(validFiles) == 0 { + fmt.Println("No valid files to upload.") + return + } + paramsRaw, ok := response["getuploadparams"] + if !ok || reflect.TypeOf(paramsRaw).Kind() != reflect.Map { + fmt.Println("Invalid response format for getuploadparams.") + return + } + params := paramsRaw.(map[string]interface{}) + requiredKeys := []string{"postURL", "metadata", "signature", "expires"} + for _, key := range requiredKeys { + if _, ok := params[key]; !ok { + fmt.Printf("Missing required key '%s' in getuploadparams response.\n", key) + return + } + } + + postURL, _ := params["postURL"].(string) + signature, _ := params["signature"].(string) + expires, _ := params["expires"].(string) + metadata, _ := params["metadata"].(string) + + fmt.Println("Uploading files for", api, ":", validFiles) + spinner := r.Config.StartSpinner("uploading files, please wait...") + defer r.Config.StopSpinner(spinner) + + for _, filePath := range validFiles { + if err := uploadFile(postURL, filePath, signature, expires, metadata); err != nil { + fmt.Println("Error uploading", filePath, ":", err) + } + } +} + +// uploadFile uploads a single file to the given postURL with the required headers. +func uploadFile(postURL, filePath, signature, expires, metadata string) error { + file, err := os.Open(filePath) + if err != nil { + return err + } + defer file.Close() + + var body bytes.Buffer + writer := multipart.NewWriter(&body) + part, err := writer.CreateFormFile("file", filepath.Base(filePath)) + if err != nil { + return err + } + if _, err := io.Copy(part, file); err != nil { + return err + } + writer.Close() + + req, err := http.NewRequest("POST", postURL, &body) + if err != nil { + return err + } + req.Header.Set("Content-Type", writer.FormDataContentType()) + req.Header.Set("x-signature", signature) + req.Header.Set("x-expires", expires) + req.Header.Set("x-metadata", metadata) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + respBody, _ := io.ReadAll(resp.Body) + return fmt.Errorf("upload failed: %s", string(respBody)) + } + fmt.Println("Upload successful for:", filePath) + return nil +} From fbad2aabea31f6b12b6b95fae8e3ccd1ee06706e Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 7 Aug 2025 18:07:29 +0530 Subject: [PATCH 2/3] refactor, format Signed-off-by: Abhishek Kumar --- cmd/api.go | 116 +--------------------------- cmd/fileupload.go | 190 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+), 115 deletions(-) create mode 100644 cmd/fileupload.go diff --git a/cmd/api.go b/cmd/api.go index 479a92b..afbf84a 100644 --- a/cmd/api.go +++ b/cmd/api.go @@ -18,15 +18,8 @@ package cmd import ( - "bytes" "errors" "fmt" - "io" - "mime/multipart" - "net/http" - "os" - "path/filepath" - "reflect" "strings" ) @@ -118,116 +111,9 @@ func init() { if len(response) > 0 { printResult(r.Config.Core.Output, response, filterKeys, excludeKeys) - if r.Config.HasShell { - apiName := strings.ToLower(api.Name) - if apiName == "getuploadparamsforiso" || - apiName == "getuploadparamsforvolume" || - apiName == "getuploadparamsfotemplate" { - promptForFileUpload(r, apiName, response) - } - } + PromptAndUploadFileIfNeeded(r, api.Name, response) } return nil }, } } - -func promptForFileUpload(r *Request, api string, response map[string]interface{}) { - fmt.Print("Enter path of the file(s) to upload (comma-separated): ") - var filePaths string - fmt.Scanln(&filePaths) - filePathsList := strings.FieldsFunc(filePaths, func(r rune) bool { return r == ',' }) - - var missingFiles []string - var validFiles []string - for _, filePath := range filePathsList { - filePath = strings.TrimSpace(filePath) - if filePath == "" { - continue - } - if _, err := os.Stat(filePath); os.IsNotExist(err) { - missingFiles = append(missingFiles, filePath) - } else { - validFiles = append(validFiles, filePath) - } - } - if len(missingFiles) > 0 { - fmt.Println("File(s) do not exist or are not accessible:", strings.Join(missingFiles, ", ")) - return - } - if len(validFiles) == 0 { - fmt.Println("No valid files to upload.") - return - } - paramsRaw, ok := response["getuploadparams"] - if !ok || reflect.TypeOf(paramsRaw).Kind() != reflect.Map { - fmt.Println("Invalid response format for getuploadparams.") - return - } - params := paramsRaw.(map[string]interface{}) - requiredKeys := []string{"postURL", "metadata", "signature", "expires"} - for _, key := range requiredKeys { - if _, ok := params[key]; !ok { - fmt.Printf("Missing required key '%s' in getuploadparams response.\n", key) - return - } - } - - postURL, _ := params["postURL"].(string) - signature, _ := params["signature"].(string) - expires, _ := params["expires"].(string) - metadata, _ := params["metadata"].(string) - - fmt.Println("Uploading files for", api, ":", validFiles) - spinner := r.Config.StartSpinner("uploading files, please wait...") - defer r.Config.StopSpinner(spinner) - - for _, filePath := range validFiles { - if err := uploadFile(postURL, filePath, signature, expires, metadata); err != nil { - fmt.Println("Error uploading", filePath, ":", err) - } - } -} - -// uploadFile uploads a single file to the given postURL with the required headers. -func uploadFile(postURL, filePath, signature, expires, metadata string) error { - file, err := os.Open(filePath) - if err != nil { - return err - } - defer file.Close() - - var body bytes.Buffer - writer := multipart.NewWriter(&body) - part, err := writer.CreateFormFile("file", filepath.Base(filePath)) - if err != nil { - return err - } - if _, err := io.Copy(part, file); err != nil { - return err - } - writer.Close() - - req, err := http.NewRequest("POST", postURL, &body) - if err != nil { - return err - } - req.Header.Set("Content-Type", writer.FormDataContentType()) - req.Header.Set("x-signature", signature) - req.Header.Set("x-expires", expires) - req.Header.Set("x-metadata", metadata) - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { - respBody, _ := io.ReadAll(resp.Body) - return fmt.Errorf("upload failed: %s", string(respBody)) - } - fmt.Println("Upload successful for:", filePath) - return nil -} diff --git a/cmd/fileupload.go b/cmd/fileupload.go new file mode 100644 index 0000000..a437828 --- /dev/null +++ b/cmd/fileupload.go @@ -0,0 +1,190 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package cmd + +import ( + "bytes" + "fmt" + "io" + "mime/multipart" + "net/http" + "os" + "path/filepath" + "reflect" + "strings" + + "github.com/briandowns/spinner" +) + +const ( + uploadingMessage = "Uploading files, please wait..." +) + +// PromptAndUploadFileIfNeeded prompts the user to provide file paths for upload and the API is getUploadParamsFor* +func PromptAndUploadFileIfNeeded(r *Request, api string, response map[string]interface{}) { + if !r.Config.HasShell { + return + } + apiName := strings.ToLower(api) + if apiName != "getuploadparamsforiso" && + apiName != "getuploadparamsforvolume" && + apiName != "getuploadparamsfotemplate" { + return + } + fmt.Print("Enter path of the file(s) to upload (comma-separated): ") + var filePaths string + fmt.Scanln(&filePaths) + filePathsList := strings.FieldsFunc(filePaths, func(r rune) bool { return r == ',' }) + + var missingFiles []string + var validFiles []string + for _, filePath := range filePathsList { + filePath = strings.TrimSpace(filePath) + if filePath == "" { + continue + } + if _, err := os.Stat(filePath); os.IsNotExist(err) { + missingFiles = append(missingFiles, filePath) + } else { + validFiles = append(validFiles, filePath) + } + } + if len(missingFiles) > 0 { + fmt.Println("File(s) do not exist or are not accessible:", strings.Join(missingFiles, ", ")) + return + } + if len(validFiles) == 0 { + fmt.Println("No valid files to upload.") + return + } + paramsRaw, ok := response["getuploadparams"] + if !ok || reflect.TypeOf(paramsRaw).Kind() != reflect.Map { + fmt.Println("Invalid response format for getuploadparams.") + return + } + params := paramsRaw.(map[string]interface{}) + requiredKeys := []string{"postURL", "metadata", "signature", "expires"} + for _, key := range requiredKeys { + if _, ok := params[key]; !ok { + fmt.Printf("Missing required key '%s' in getuploadparams response.\n", key) + return + } + } + + postURL, _ := params["postURL"].(string) + signature, _ := params["signature"].(string) + expires, _ := params["expires"].(string) + metadata, _ := params["metadata"].(string) + + fmt.Println("Uploading files for", api, ":", validFiles) + spinner := r.Config.StartSpinner(uploadingMessage) + errored := 0 + for i, filePath := range validFiles { + spinner.Suffix = fmt.Sprintf(" uploading %d/%d %s...", i+1, len(validFiles), filepath.Base(filePath)) + if err := uploadFile(postURL, filePath, signature, expires, metadata, spinner); err != nil { + spinner.Stop() + fmt.Println("Error uploading", filePath, ":", err) + errored++ + spinner.Suffix = fmt.Sprintf(" %s", uploadingMessage) + spinner.Start() + } + } + r.Config.StopSpinner(spinner) + if errored > 0 { + fmt.Printf("🙈 %d out of %d files failed to upload.\n", errored, len(validFiles)) + } else { + fmt.Println("All files uploaded successfully.") + } +} + +type progressReader struct { + file *os.File + total int64 + read int64 + updateSuffix func(percent int) +} + +func (pr *progressReader) Read(p []byte) (int, error) { + n, err := pr.file.Read(p) + if n > 0 { + pr.read += int64(n) + percent := int(float64(pr.read) / float64(pr.total) * 100) + pr.updateSuffix(percent) + } + return n, err +} + +// uploadFile uploads a single file to the given postURL with the required headers. +func uploadFile(postURL, filePath, signature, expires, metadata string, spinner *spinner.Spinner) error { + originalSuffix := spinner.Suffix + file, err := os.Open(filePath) + if err != nil { + return err + } + defer file.Close() + + fileInfo, err := file.Stat() + if err != nil { + return err + } + + var body bytes.Buffer + writer := multipart.NewWriter(&body) + part, err := writer.CreateFormFile("file", filepath.Base(filePath)) + if err != nil { + return err + } + + pr := &progressReader{ + file: file, + total: fileInfo.Size(), + updateSuffix: func(percent int) { + spinner.Suffix = fmt.Sprintf(" %s (%d%%)", originalSuffix, percent) + }, + } + if _, err := io.Copy(part, pr); err != nil { + return err + } + writer.Close() + + req, err := http.NewRequest("POST", postURL, &body) + if err != nil { + return err + } + req.Header.Set("Content-Type", writer.FormDataContentType()) + req.Header.Set("x-signature", signature) + req.Header.Set("x-expires", expires) + req.Header.Set("x-metadata", metadata) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + respBody, _ := io.ReadAll(resp.Body) + return fmt.Errorf("upload failed: %s", string(respBody)) + } + spinner.Stop() + fmt.Println("Upload successful for:", filePath) + spinner.Suffix = fmt.Sprintf(" %s", uploadingMessage) + spinner.Start() + return nil +} From ea7f30c2a5008620d94487f624137b95dc7a0a6c Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 13 Aug 2025 10:49:02 +0530 Subject: [PATCH 3/3] changes for adding argument Signed-off-by: Abhishek Kumar --- cmd/api.go | 22 +++++- cmd/fileupload.go | 187 +++++++++++++++++++++++++++++++--------------- cmd/network.go | 14 ++++ config/cache.go | 39 ++++++++-- 4 files changed, 194 insertions(+), 68 deletions(-) diff --git a/cmd/api.go b/cmd/api.go index afbf84a..f7b9149 100644 --- a/cmd/api.go +++ b/cmd/api.go @@ -21,6 +21,8 @@ import ( "errors" "fmt" "strings" + + "github.com/apache/cloudstack-cloudmonkey/config" ) var apiCommand *Command @@ -46,11 +48,23 @@ func init() { apiArgs = r.Args[2:] } - for _, arg := range r.Args { + var uploadFiles []string + + for _, arg := range apiArgs { if arg == "-h" { r.Args[0] = apiName return helpCommand.Handle(r) } + if strings.HasPrefix(arg, config.FilePathArg) && config.IsFileUploadAPI(apiName) { + var err error + uploadFiles, err = ValidateAndGetFileList(arg[len(config.FilePathArg):]) + if err != nil { + return err + } + if len(uploadFiles) == 0 { + return errors.New("no valid files to upload") + } + } } api := r.Config.GetCache()[apiName] @@ -111,7 +125,11 @@ func init() { if len(response) > 0 { printResult(r.Config.Core.Output, response, filterKeys, excludeKeys) - PromptAndUploadFileIfNeeded(r, api.Name, response) + if len(uploadFiles) > 0 { + UploadFiles(r, api.Name, response, uploadFiles) + } else { + PromptAndUploadFilesIfNeeded(r, api.Name, response) + } } return nil }, diff --git a/cmd/fileupload.go b/cmd/fileupload.go index a437828..30bf986 100644 --- a/cmd/fileupload.go +++ b/cmd/fileupload.go @@ -18,7 +18,6 @@ package cmd import ( - "bytes" "fmt" "io" "mime/multipart" @@ -27,28 +26,20 @@ import ( "path/filepath" "reflect" "strings" + "time" + "github.com/apache/cloudstack-cloudmonkey/config" "github.com/briandowns/spinner" ) const ( - uploadingMessage = "Uploading files, please wait..." + uploadingMessage = "Uploading files, please wait..." + progressCharCount = 24 ) -// PromptAndUploadFileIfNeeded prompts the user to provide file paths for upload and the API is getUploadParamsFor* -func PromptAndUploadFileIfNeeded(r *Request, api string, response map[string]interface{}) { - if !r.Config.HasShell { - return - } - apiName := strings.ToLower(api) - if apiName != "getuploadparamsforiso" && - apiName != "getuploadparamsforvolume" && - apiName != "getuploadparamsfotemplate" { - return - } - fmt.Print("Enter path of the file(s) to upload (comma-separated): ") - var filePaths string - fmt.Scanln(&filePaths) +// ValidateAndGetFileList parses a comma-separated string of file paths, trims them, +// checks for existence, and returns a slice of valid file paths or an error if any are missing. +func ValidateAndGetFileList(filePaths string) ([]string, error) { filePathsList := strings.FieldsFunc(filePaths, func(r rune) bool { return r == ',' }) var missingFiles []string @@ -65,13 +56,41 @@ func PromptAndUploadFileIfNeeded(r *Request, api string, response map[string]int } } if len(missingFiles) > 0 { - fmt.Println("File(s) do not exist or are not accessible:", strings.Join(missingFiles, ", ")) + return nil, fmt.Errorf("file(s) do not exist or are not accessible: %s", strings.Join(missingFiles, ", ")) + } + return validFiles, nil +} + +// PromptAndUploadFilesIfNeeded prompts the user to provide file paths for upload and the API is getUploadParamsFor* +func PromptAndUploadFilesIfNeeded(r *Request, api string, response map[string]interface{}) { + if !r.Config.HasShell { + return + } + apiName := strings.ToLower(api) + if !config.IsFileUploadAPI(apiName) { + return + } + fmt.Print("Enter path of the file(s) to upload (comma-separated), leave empty to skip: ") + var filePaths string + fmt.Scanln(&filePaths) + if filePaths == "" { + return + } + validFiles, err := ValidateAndGetFileList(filePaths) + if err != nil { + fmt.Println(err) return } if len(validFiles) == 0 { fmt.Println("No valid files to upload.") return } + UploadFiles(r, api, response, validFiles) +} + +// UploadFiles uploads files to a remote server using parameters from the API response. +// Shows progress for each file and reports any failures. +func UploadFiles(r *Request, api string, response map[string]interface{}, validFiles []string) { paramsRaw, ok := response["getuploadparams"] if !ok || reflect.TypeOf(paramsRaw).Kind() != reflect.Map { fmt.Println("Invalid response format for getuploadparams.") @@ -85,7 +104,6 @@ func PromptAndUploadFileIfNeeded(r *Request, api string, response map[string]int return } } - postURL, _ := params["postURL"].(string) signature, _ := params["signature"].(string) expires, _ := params["expires"].(string) @@ -96,7 +114,7 @@ func PromptAndUploadFileIfNeeded(r *Request, api string, response map[string]int errored := 0 for i, filePath := range validFiles { spinner.Suffix = fmt.Sprintf(" uploading %d/%d %s...", i+1, len(validFiles), filepath.Base(filePath)) - if err := uploadFile(postURL, filePath, signature, expires, metadata, spinner); err != nil { + if err := uploadFile(i, len(validFiles), postURL, filePath, signature, expires, metadata, spinner); err != nil { spinner.Stop() fmt.Println("Error uploading", filePath, ":", err) errored++ @@ -112,79 +130,126 @@ func PromptAndUploadFileIfNeeded(r *Request, api string, response map[string]int } } -type progressReader struct { - file *os.File - total int64 - read int64 - updateSuffix func(percent int) +// progressReader streams file data and updates progress as bytes are read. +type progressBody struct { + f *os.File + read int64 + total int64 + update func(int) } -func (pr *progressReader) Read(p []byte) (int, error) { - n, err := pr.file.Read(p) +func (pb *progressBody) Read(p []byte) (int, error) { + n, err := pb.f.Read(p) if n > 0 { - pr.read += int64(n) - percent := int(float64(pr.read) / float64(pr.total) * 100) - pr.updateSuffix(percent) + pb.read += int64(n) + pct := int(float64(pb.read) * 100 / float64(pb.total)) + pb.update(pct) } return n, err } +func (pb *progressBody) Close() error { return pb.f.Close() } + +func barArrow(pct int) string { + width := progressCharCount + if pct < 0 { + pct = 0 + } + if pct > 100 { + pct = 100 + } + pos := (pct * width) / 100 + // 100%: full bar, no head + if pos >= width { + return fmt.Sprintf("[%s]", + strings.Repeat("=", width)) + } + left := strings.Repeat("=", pos) + ">" + right := strings.Repeat(" ", width-pos-1) + + return fmt.Sprintf("[%s%s]", left, right) +} -// uploadFile uploads a single file to the given postURL with the required headers. -func uploadFile(postURL, filePath, signature, expires, metadata string, spinner *spinner.Spinner) error { - originalSuffix := spinner.Suffix - file, err := os.Open(filePath) +// uploadFile streams a large file to the server with progress updates. +func uploadFile(index, count int, postURL, filePath, signature, expires, metadata string, spn *spinner.Spinner) error { + fileName := filepath.Base(filePath) + in, err := os.Open(filePath) if err != nil { return err } - defer file.Close() - - fileInfo, err := file.Stat() + defer in.Close() + _, err = in.Stat() if err != nil { return err } - - var body bytes.Buffer - writer := multipart.NewWriter(&body) - part, err := writer.CreateFormFile("file", filepath.Base(filePath)) + tmp, err := os.CreateTemp("", "multipart-body-*.tmp") if err != nil { return err } - - pr := &progressReader{ - file: file, - total: fileInfo.Size(), - updateSuffix: func(percent int) { - spinner.Suffix = fmt.Sprintf(" %s (%d%%)", originalSuffix, percent) - }, + defer func() { + tmp.Close() + os.Remove(tmp.Name()) + }() + mw := multipart.NewWriter(tmp) + part, err := mw.CreateFormFile("file", filepath.Base(filePath)) + if err != nil { + return err } - if _, err := io.Copy(part, pr); err != nil { + if _, err := io.Copy(part, in); err != nil { return err } - writer.Close() - - req, err := http.NewRequest("POST", postURL, &body) + if err := mw.Close(); err != nil { + return err + } + size, err := tmp.Seek(0, io.SeekEnd) if err != nil { return err } - req.Header.Set("Content-Type", writer.FormDataContentType()) + if _, err := tmp.Seek(0, io.SeekStart); err != nil { + return err + } + req, err := http.NewRequest("POST", postURL, nil) + if err != nil { + return err + } + req.Header.Set("Content-Type", mw.FormDataContentType()) req.Header.Set("x-signature", signature) req.Header.Set("x-expires", expires) req.Header.Set("x-metadata", metadata) - - client := &http.Client{} + req.ContentLength = size + pb := &progressBody{ + f: tmp, + total: size, + update: func(pct int) { + spn.Suffix = fmt.Sprintf(" [%d/%d] %s\t%s %d%%", index+1, count, fileName, barArrow(pct), pct) + }, + } + req.Body = pb + req.GetBody = func() (io.ReadCloser, error) { + f, err := os.Open(tmp.Name()) + if err != nil { + return nil, err + } + return f, nil + } + client := &http.Client{ + Timeout: 24 * time.Hour, + Transport: &http.Transport{ + ExpectContinueTimeout: 0, + }, + } resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() - if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { - respBody, _ := io.ReadAll(resp.Body) - return fmt.Errorf("upload failed: %s", string(respBody)) + b, _ := io.ReadAll(resp.Body) + return fmt.Errorf("[%d/%d] %s\tupload failed: %s", index+1, count, fileName, string(b)) } - spinner.Stop() - fmt.Println("Upload successful for:", filePath) - spinner.Suffix = fmt.Sprintf(" %s", uploadingMessage) - spinner.Start() + + spn.Stop() + fmt.Printf("[%d/%d] %s\t%s ✅\n", index+1, count, fileName, barArrow(100)) + spn.Suffix = fmt.Sprintf(" %s", uploadingMessage) + spn.Start() return nil } diff --git a/cmd/network.go b/cmd/network.go index 69cf6ce..c6dfe4b 100644 --- a/cmd/network.go +++ b/cmd/network.go @@ -278,7 +278,21 @@ func pollAsyncJob(r *Request, jobID string) (map[string]interface{}, error) { func NewAPIRequest(r *Request, api string, args []string, isAsync bool) (map[string]interface{}, error) { params := make(url.Values) params.Add("command", api) + apiData := r.Config.GetCache()[api] for _, arg := range args { + if apiData != nil { + skip := false + for _, fakeArg := range apiData.FakeArgs { + if strings.HasPrefix(arg, fakeArg) { + skip = true + break + } + } + if skip { + continue + } + + } parts := strings.SplitN(arg, "=", 2) if len(parts) == 2 { key := parts[0] diff --git a/config/cache.go b/config/cache.go index 13596dd..7359427 100644 --- a/config/cache.go +++ b/config/cache.go @@ -28,7 +28,10 @@ import ( ) // FAKE is used for fake CLI only options like filter= -const FAKE = "fake" +const ( + FAKE = "fake" + FilePathArg = "filepath=" +) // APIArg are the args passable to an API type APIArg struct { @@ -47,6 +50,7 @@ type API struct { Noun string Args []*APIArg RequiredArgs []string + FakeArgs []string Related []string Async bool Description string @@ -145,18 +149,32 @@ func (c *Config) UpdateCache(response map[string]interface{}) interface{} { } // Add filter arg - apiArgs = append(apiArgs, &APIArg{ + fakeArg := &APIArg{ Name: "filter=", Type: FAKE, Description: "cloudmonkey specific response key filtering", - }) + } + apiArgs = append(apiArgs, fakeArg) + fakeArgs := []string{fakeArg.Name} // Add exclude arg - apiArgs = append(apiArgs, &APIArg{ + fakeArg = &APIArg{ Name: "exclude=", Type: FAKE, Description: "cloudmonkey specific response key to exlude when filtering", - }) + } + apiArgs = append(apiArgs, fakeArg) + fakeArgs = append(fakeArgs, fakeArg.Name) + + if IsFileUploadAPI(apiName) { + fakeArg = &APIArg{ + Name: FilePathArg, + Type: FAKE, + Description: "cloudmonkey specific key to upload files for supporting APIs. Comma-separated list of file paths can be provided", + } + apiArgs = append(apiArgs, fakeArg) + fakeArgs = append(fakeArgs, fakeArg.Name) + } sort.Slice(apiArgs, func(i, j int) bool { return apiArgs[i].Name < apiArgs[j].Name @@ -186,6 +204,7 @@ func (c *Config) UpdateCache(response map[string]interface{}) interface{} { Noun: noun, Args: apiArgs, RequiredArgs: requiredArgs, + FakeArgs: fakeArgs, Async: isAsync, Description: description, ResponseKeys: responseKeys, @@ -193,3 +212,13 @@ func (c *Config) UpdateCache(response map[string]interface{}) interface{} { } return count } + +// IsFileUploadAPI checks if the provided API name corresponds to a file upload-related API. +// It returns true if the API name matches one of the following (case-insensitive): +// "getUploadParamsForIso", "getUploadParamsForVolume", or "getUploadParamsForTemplate". +func IsFileUploadAPI(api string) bool { + apiName := strings.ToLower(api) + return apiName == "getuploadparamsforiso" || + apiName == "getuploadparamsforvolume" || + apiName == "getuploadparamsfortemplate" +}