github.com/vmware/go-vcloud-director/v2@v2.24.0/govcd/upload.go (about) 1 /* 2 * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. 3 */ 4 5 package govcd 6 7 import ( 8 "bytes" 9 "errors" 10 "fmt" 11 "io" 12 "net/http" 13 "net/url" 14 "os" 15 "path/filepath" 16 "strconv" 17 "sync" 18 19 "github.com/vmware/go-vcloud-director/v2/types/v56" 20 "github.com/vmware/go-vcloud-director/v2/util" 21 ) 22 23 // mutexedProgress is a thread-safe structure to update and report progress during an UploadTask. 24 // 25 // Value must be read/written using LockedGet/LockedSet values instead of directly accessing the `progress` variable 26 type mutexedProgress struct { 27 progress float64 28 sync.Mutex 29 } 30 31 func (p *mutexedProgress) LockedSet(progress float64) { 32 p.Lock() 33 defer p.Unlock() 34 p.progress = progress 35 } 36 37 func (p *mutexedProgress) LockedGet() float64 { 38 p.Lock() 39 defer p.Unlock() 40 return p.progress 41 } 42 43 // uploadLink - vCD created temporary upload link 44 // uploadedBytes - how much of file already uploaded 45 // fileSizeToUpload - how much bytes will be uploaded 46 // uploadPieceSize - size of chunks in which the file will be uploaded to the catalog. 47 // uploadedBytesForCallback all uploaded bytes if multi disk in ova 48 // allFilesSize overall sum of size if multi disk in ova 49 // callBack a function with signature //function(bytesUpload, totalSize) to let the caller monitor progress of the upload operation. 50 type uploadDetails struct { 51 uploadLink string 52 uploadedBytes, fileSizeToUpload, uploadPieceSize, uploadedBytesForCallback, allFilesSize int64 53 callBack func(bytesUpload, totalSize int64) 54 uploadError *error 55 } 56 57 // Upload file by parts which size is defined by user provided variable uploadPieceSize and 58 // provides how much bytes uploaded to callback. Callback allows to monitor upload progress. 59 // params: 60 // client - client for requests 61 // filePath - file path to file which will be uploaded 62 // uploadDetails - file upload settings and data 63 func uploadFile(client *Client, filePath string, uDetails uploadDetails) (int64, error) { 64 util.Logger.Printf("[TRACE] Starting uploading: %s, offset: %v, fileze: %v, toLink: %s \n", filePath, uDetails.uploadedBytes, uDetails.fileSizeToUpload, uDetails.uploadLink) 65 66 var part []byte 67 var count int 68 var pieceSize int64 69 70 file, err := os.Open(filepath.Clean(filePath)) 71 if err != nil { 72 util.Logger.Printf("[ERROR] during upload process - file open issue : %s, error %s ", filePath, err) 73 *uDetails.uploadError = err 74 return 0, err 75 } 76 77 fileInfo, err := file.Stat() 78 if err != nil { 79 util.Logger.Printf("[ERROR] during upload process - file issue : %s, error %s ", filePath, err) 80 *uDetails.uploadError = err 81 return 0, err 82 } 83 84 defer safeClose(file) 85 86 fileSize := fileInfo.Size() 87 // when file size in OVF does not exist, use real file size instead 88 if uDetails.fileSizeToUpload == -1 { 89 uDetails.fileSizeToUpload = fileSize 90 uDetails.allFilesSize += fileSize 91 } 92 // TODO: file size in OVF maybe wrong? how to handle that? 93 if uDetails.fileSizeToUpload != fileSize { 94 fmt.Printf("WARNING:file size %d in OVF is not align with real file size %d, upload task may hung.\n", 95 uDetails.fileSizeToUpload, fileSize) 96 } 97 98 // do not allow smaller than 1kb 99 if uDetails.uploadPieceSize > 1024 && uDetails.uploadPieceSize < uDetails.fileSizeToUpload { 100 pieceSize = uDetails.uploadPieceSize 101 } else { 102 pieceSize = defaultPieceSize 103 } 104 105 util.Logger.Printf("[TRACE] Uploading will use piece size: %#v \n", pieceSize) 106 part = make([]byte, pieceSize) 107 108 for { 109 if count, err = io.ReadFull(file, part); err != nil { 110 break 111 } 112 err = uploadPartFile(client, part, int64(count), uDetails) 113 uDetails.uploadedBytes += int64(count) 114 uDetails.uploadedBytesForCallback += int64(count) 115 if err != nil { 116 util.Logger.Printf("[ERROR] during upload process: %s, error %s ", filePath, err) 117 *uDetails.uploadError = err 118 return 0, err 119 } 120 } 121 122 // upload last part as ReadFull returns io.ErrUnexpectedEOF when reaches end of file. 123 if err == io.ErrUnexpectedEOF { 124 err = uploadPartFile(client, part[:count], int64(count), uDetails) 125 if err != nil { 126 util.Logger.Printf("[ERROR] during upload process: %s, error %s ", filePath, err) 127 *uDetails.uploadError = err 128 return 0, err 129 } 130 } else { 131 util.Logger.Printf("Error Uploading: %s, error %s ", filePath, err) 132 *uDetails.uploadError = err 133 return 0, err 134 } 135 136 return fileSize, nil 137 } 138 139 // Create Request with right headers and range settings. Support multi part file upload. 140 // client - client for requests 141 // requestUrl - upload url 142 // filePart - bytes to upload 143 // offset - how much is uploaded 144 // filePartSize - how much bytes will be uploaded 145 // fileSizeToUpload - final file size 146 func newFileUploadRequest(client *Client, requestUrl string, filePart []byte, offset, filePartSize, fileSizeToUpload int64) (*http.Request, error) { 147 util.Logger.Printf("[TRACE] Creating file upload request: %s, %v, %v, %v \n", requestUrl, offset, filePartSize, fileSizeToUpload) 148 149 parsedRequestURL, err := url.ParseRequestURI(requestUrl) 150 if err != nil { 151 return nil, fmt.Errorf("error decoding vdc response: %s", err) 152 } 153 154 uploadReq := client.NewRequestWitNotEncodedParams(nil, nil, http.MethodPut, *parsedRequestURL, bytes.NewReader(filePart)) 155 156 uploadReq.ContentLength = filePartSize 157 uploadReq.Header.Set("Content-Length", strconv.FormatInt(uploadReq.ContentLength, 10)) 158 159 rangeExpression := "bytes " + strconv.FormatInt(int64(offset), 10) + "-" + strconv.FormatInt(int64(offset+filePartSize-1), 10) + "/" + strconv.FormatInt(int64(fileSizeToUpload), 10) 160 uploadReq.Header.Set("Content-Range", rangeExpression) 161 162 for key, value := range uploadReq.Header { 163 util.Logger.Printf("[TRACE] Header: %s :%s \n", key, value) 164 } 165 166 return uploadReq, nil 167 } 168 169 // Initiates file part upload by creating request and running it. 170 // params: 171 // client - client for requests 172 // part - bytes of file part 173 // partDataSize - how much bytes will be uploaded 174 // uploadDetails - file upload settings and data 175 func uploadPartFile(client *Client, part []byte, partDataSize int64, uDetails uploadDetails) error { 176 // Avoids session time out, as the multi part upload is treated as one request 177 makeEmptyRequest(client) 178 request, err := newFileUploadRequest(client, uDetails.uploadLink, part, uDetails.uploadedBytes, partDataSize, uDetails.fileSizeToUpload) 179 if err != nil { 180 return err 181 } 182 183 response, err := checkResp(client.Http.Do(request)) 184 if err != nil { 185 return fmt.Errorf("file upload failed. Err: %s", err) 186 } 187 err = response.Body.Close() 188 if err != nil { 189 return fmt.Errorf("file closing failed. Err: %s", err) 190 } 191 192 uDetails.callBack(uDetails.uploadedBytesForCallback+partDataSize, uDetails.allFilesSize) 193 194 return nil 195 } 196 197 // call query for task which are very fast and optimised as UI calls it very often 198 func makeEmptyRequest(client *Client) { 199 apiEndpoint := client.VCDHREF 200 apiEndpoint.Path += "/query?type=task&format=records&page=1&pageSize=5&" 201 202 _, err := client.ExecuteRequest(apiEndpoint.String(), http.MethodGet, 203 "", "error making empty request: %s", nil, nil) 204 if err != nil { 205 util.Logger.Printf("[DEBUG - makeEmptyRequest] error executing request: %s", err) 206 } 207 } 208 209 func getUploadLink(files *types.FilesList) (*url.URL, error) { 210 util.Logger.Printf("[TRACE] getUploadLink - Parsing upload link: %#v\n", files) 211 212 if len(files.File) > 1 { 213 return nil, errors.New("unexpected response from vCD: found more than one link for upload") 214 } 215 216 ovfUploadHref, err := url.ParseRequestURI(files.File[0].Link[0].HREF) 217 if err != nil { 218 return nil, err 219 } 220 221 util.Logger.Printf("[TRACE] getUploadLink- upload link found: %#v\n", ovfUploadHref) 222 return ovfUploadHref, nil 223 } 224 225 func createTaskForVcdImport(client *Client, taskHREF string) (Task, error) { 226 util.Logger.Printf("[TRACE] Create task for vcd with HREF: %s\n", taskHREF) 227 228 taskURL, err := url.ParseRequestURI(taskHREF) 229 if err != nil { 230 return Task{}, err 231 } 232 233 request := client.NewRequest(map[string]string{}, http.MethodGet, *taskURL, nil) 234 response, err := checkResp(client.Http.Do(request)) 235 if err != nil { 236 return Task{}, err 237 } 238 239 task := NewTask(client) 240 241 if err = decodeBody(types.BodyTypeXML, response, task.Task); err != nil { 242 return Task{}, fmt.Errorf("error decoding Task response: %s", err) 243 } 244 245 // The request was successful 246 return *task, nil 247 } 248 249 func getProgressCallBackFunction() (func(int64, int64), *mutexedProgress) { 250 uploadProgress := &mutexedProgress{} 251 callback := func(bytesUploaded, totalSize int64) { 252 uploadProgress.LockedSet((float64(bytesUploaded) / float64(totalSize)) * 100) 253 } 254 return callback, uploadProgress 255 } 256 257 func validateAndFixFilePath(file string) (string, error) { 258 absolutePath, err := filepath.Abs(file) 259 if err != nil { 260 return "", err 261 } 262 fileInfo, err := os.Stat(absolutePath) 263 if os.IsNotExist(err) { 264 return "", err 265 } 266 if fileInfo.Size() == 0 { 267 return "", errors.New("file is empty") 268 } 269 return absolutePath, nil 270 }