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  }