github.com/vmware/go-vcloud-director/v2@v2.24.0/govcd/media.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  	"strings"
    18  	"time"
    19  
    20  	"github.com/vmware/go-vcloud-director/v2/types/v56"
    21  	"github.com/vmware/go-vcloud-director/v2/util"
    22  )
    23  
    24  // Deprecated: use MediaRecord
    25  type MediaItem struct {
    26  	MediaItem *types.MediaRecordType
    27  	vdc       *Vdc
    28  }
    29  
    30  // Deprecated: use NewMediaRecord
    31  func NewMediaItem(vdc *Vdc) *MediaItem {
    32  	return &MediaItem{
    33  		MediaItem: new(types.MediaRecordType),
    34  		vdc:       vdc,
    35  	}
    36  }
    37  
    38  type Media struct {
    39  	Media  *types.Media
    40  	client *Client
    41  }
    42  
    43  func NewMedia(cli *Client) *Media {
    44  	return &Media{
    45  		Media:  new(types.Media),
    46  		client: cli,
    47  	}
    48  }
    49  
    50  type MediaRecord struct {
    51  	MediaRecord *types.MediaRecordType
    52  	client      *Client
    53  }
    54  
    55  func NewMediaRecord(cli *Client) *MediaRecord {
    56  	return &MediaRecord{
    57  		MediaRecord: new(types.MediaRecordType),
    58  		client:      cli,
    59  	}
    60  }
    61  
    62  // Uploads an ISO file as media. This method only uploads bits to vCD spool area.
    63  // Returns errors if any occur during upload from vCD or upload process. On upload fail client may need to
    64  // remove vCD catalog item which waits for files to be uploaded.
    65  //
    66  // Deprecated: This method is broken in API V32.0+. Please use catalog.UploadMediaImage because VCD does not support
    67  // uploading directly to VDC anymore.
    68  func (vdc *Vdc) UploadMediaImage(mediaName, mediaDescription, filePath string, uploadPieceSize int64) (UploadTask, error) {
    69  	util.Logger.Printf("[TRACE] UploadImage: %s, image name: %v \n", mediaName, mediaDescription)
    70  
    71  	//	On a very high level the flow is as follows
    72  	//	1. Makes a POST call to vCD to create media item(also creates a transfer folder in the spool area and as result will give a media item resource XML).
    73  	//	2. Start uploading bits to the transfer folder
    74  	//	3. Wait on the import task to finish on vCD side -> task success = upload complete
    75  
    76  	if *vdc == (Vdc{}) {
    77  		return UploadTask{}, errors.New("vdc can not be empty or nil")
    78  	}
    79  
    80  	mediaFilePath, err := validateAndFixFilePath(filePath)
    81  	if err != nil {
    82  		return UploadTask{}, err
    83  	}
    84  
    85  	isISOGood, err := verifyIso(mediaFilePath)
    86  	if err != nil || !isISOGood {
    87  		return UploadTask{}, fmt.Errorf("[ERROR] File %s isn't correct iso file: %s", mediaFilePath, err)
    88  	}
    89  
    90  	mediaList, err := getExistingMedia(vdc)
    91  	if err != nil {
    92  		return UploadTask{}, fmt.Errorf("[ERROR] Checking existing media files failed: %s", err)
    93  	}
    94  
    95  	for _, media := range mediaList {
    96  		if media.Name == mediaName {
    97  			return UploadTask{}, fmt.Errorf("media item '%s' already exists. Upload with different name", mediaName)
    98  		}
    99  	}
   100  
   101  	file, e := os.Stat(mediaFilePath)
   102  	if e != nil {
   103  		return UploadTask{}, fmt.Errorf("[ERROR] Issue finding file: %#v", e)
   104  	}
   105  	fileSize := file.Size()
   106  
   107  	media, err := createMedia(vdc.client, vdc.Vdc.HREF+"/media", mediaName, mediaDescription, fileSize)
   108  	if err != nil {
   109  		return UploadTask{}, fmt.Errorf("[ERROR] Issue creating media: %s", err)
   110  	}
   111  
   112  	return executeUpload(vdc.client, media, mediaFilePath, mediaName, fileSize, uploadPieceSize)
   113  }
   114  
   115  func executeUpload(client *Client, media *types.Media, mediaFilePath, mediaName string, fileSize, uploadPieceSize int64) (UploadTask, error) {
   116  	uploadLink, err := getUploadLink(media.Files)
   117  	if err != nil {
   118  		return UploadTask{}, fmt.Errorf("[ERROR] Issue getting upload link: %s", err)
   119  	}
   120  
   121  	callBack, uploadProgress := getProgressCallBackFunction()
   122  
   123  	uploadError := *new(error)
   124  
   125  	details := uploadDetails{
   126  		uploadLink:               uploadLink.String(), // just take string
   127  		uploadedBytes:            0,
   128  		fileSizeToUpload:         fileSize,
   129  		uploadPieceSize:          uploadPieceSize,
   130  		uploadedBytesForCallback: 0,
   131  		allFilesSize:             fileSize,
   132  		callBack:                 callBack,
   133  		uploadError:              &uploadError,
   134  	}
   135  
   136  	// sending upload process to background, this allows not to lock and return task to client
   137  	// The error should be captured in details.uploadError, but just in case, we add a logging for the
   138  	// main error
   139  	go func() {
   140  		_, err = uploadFile(client, mediaFilePath, details)
   141  		if err != nil {
   142  			util.Logger.Println(strings.Repeat("*", 80))
   143  			util.Logger.Printf("*** [DEBUG - executeUpload] error calling uploadFile: %s\n", err)
   144  			util.Logger.Println(strings.Repeat("*", 80))
   145  		}
   146  	}()
   147  
   148  	var task Task
   149  	for _, item := range media.Tasks.Task {
   150  		task, err = createTaskForVcdImport(client, item.HREF)
   151  		if err != nil {
   152  			removeImageOnError(client, media, mediaName)
   153  			return UploadTask{}, err
   154  		}
   155  		if task.Task.Status == "error" {
   156  			removeImageOnError(client, media, mediaName)
   157  			return UploadTask{}, fmt.Errorf("task did not complete succesfully: %s", task.Task.Description)
   158  		}
   159  	}
   160  
   161  	uploadTask := NewUploadTask(&task, uploadProgress, &uploadError)
   162  
   163  	util.Logger.Printf("[TRACE] Upload media function finished and task for vcd import created. \n")
   164  
   165  	return *uploadTask, nil
   166  }
   167  
   168  // Initiates creation of media item and returns temporary upload URL.
   169  func createMedia(client *Client, link, mediaName, mediaDescription string, fileSize int64) (*types.Media, error) {
   170  	uploadUrl, err := url.ParseRequestURI(link)
   171  	if err != nil {
   172  		return nil, fmt.Errorf("error getting vdc href: %s", err)
   173  	}
   174  
   175  	reqBody := bytes.NewBufferString(
   176  		"<Media xmlns=\"" + types.XMLNamespaceVCloud + "\" name=\"" + mediaName + "\" imageType=\"" + "iso" + "\" size=\"" + strconv.FormatInt(fileSize, 10) + "\" >" +
   177  			"<Description>" + mediaDescription + "</Description>" +
   178  			"</Media>")
   179  
   180  	request := client.NewRequest(map[string]string{}, http.MethodPost, *uploadUrl, reqBody)
   181  	request.Header.Add("Content-Type", "application/vnd.vmware.vcloud.media+xml")
   182  
   183  	response, err := checkResp(client.Http.Do(request))
   184  	if err != nil {
   185  		return nil, err
   186  	}
   187  	defer func(Body io.ReadCloser) {
   188  		err := Body.Close()
   189  		if err != nil {
   190  			util.Logger.Printf("error closing response Body [createMedia]: %s", err)
   191  		}
   192  	}(response.Body)
   193  
   194  	mediaForUpload := &types.Media{}
   195  	if err = decodeBody(types.BodyTypeXML, response, mediaForUpload); err != nil {
   196  		return nil, err
   197  	}
   198  
   199  	util.Logger.Printf("[TRACE] Media item parsed: %#v\n", mediaForUpload)
   200  
   201  	if mediaForUpload.Tasks != nil {
   202  		for _, task := range mediaForUpload.Tasks.Task {
   203  			if task.Status == "error" && mediaName == mediaForUpload.Name {
   204  				util.Logger.Printf("[Error] issue with creating media %#v", task.Error)
   205  				return nil, fmt.Errorf("error in vcd returned error code: %d, error: %s and message: %s ", task.Error.MajorErrorCode, task.Error.MinorErrorCode, task.Error.Message)
   206  			}
   207  		}
   208  	}
   209  
   210  	return mediaForUpload, nil
   211  }
   212  
   213  func removeImageOnError(client *Client, media *types.Media, itemName string) {
   214  	if media != nil {
   215  		util.Logger.Printf("[TRACE] Deleting media item %#v", media)
   216  
   217  		// wait for task, cancel it and media item will be removed.
   218  		var err error
   219  		for {
   220  			util.Logger.Printf("[TRACE] Sleep... for 5 seconds.\n")
   221  			time.Sleep(time.Second * 5)
   222  			media, err = queryMedia(client, media.HREF, itemName)
   223  			if err != nil {
   224  				util.Logger.Printf("[Error] Error deleting media item %v: %s", media, err)
   225  			}
   226  			if len(media.Tasks.Task) > 0 {
   227  				util.Logger.Printf("[TRACE] Task found. Will try to cancel.\n")
   228  				break
   229  			}
   230  		}
   231  
   232  		for _, taskItem := range media.Tasks.Task {
   233  			if itemName == taskItem.Owner.Name {
   234  				task := NewTask(client)
   235  				task.Task = taskItem
   236  				err = task.CancelTask()
   237  				if err != nil {
   238  					util.Logger.Printf("[ERROR] Error canceling task for media upload %s", err)
   239  				}
   240  			}
   241  		}
   242  	} else {
   243  		util.Logger.Printf("[Error] Failed to delete media item created with error: %v", media)
   244  	}
   245  }
   246  
   247  func queryMedia(client *Client, mediaUrl string, newItemName string) (*types.Media, error) {
   248  	util.Logger.Printf("[TRACE] Querying media: %s\n", mediaUrl)
   249  
   250  	mediaParsed := &types.Media{}
   251  
   252  	_, err := client.ExecuteRequest(mediaUrl, http.MethodGet,
   253  		"", "error quering media: %s", nil, mediaParsed)
   254  	if err != nil {
   255  		return nil, err
   256  	}
   257  
   258  	for _, task := range mediaParsed.Tasks.Task {
   259  		if task.Status == "error" && newItemName == task.Owner.Name {
   260  			util.Logger.Printf("[Error] %#v", task.Error)
   261  			return mediaParsed, fmt.Errorf("error in vcd returned error code: %d, error: %s and message: %s ", task.Error.MajorErrorCode, task.Error.MinorErrorCode, task.Error.Message)
   262  		}
   263  	}
   264  
   265  	return mediaParsed, nil
   266  }
   267  
   268  // Verifies provided file header matches standard
   269  func verifyIso(filePath string) (bool, error) {
   270  	file, err := os.Open(filepath.Clean(filePath))
   271  	if err != nil {
   272  		return false, err
   273  	}
   274  	defer safeClose(file)
   275  
   276  	return readHeader(file)
   277  }
   278  
   279  func readHeader(reader io.Reader) (bool, error) {
   280  	buffer := make([]byte, 37000)
   281  
   282  	_, err := reader.Read(buffer)
   283  	if err != nil && err != io.EOF {
   284  		return false, err
   285  	}
   286  
   287  	headerOk := verifyHeader(buffer)
   288  
   289  	if headerOk {
   290  		return true, nil
   291  	} else {
   292  		return false, errors.New("file header didn't match ISO or UDF standard")
   293  	}
   294  }
   295  
   296  // Verify file header for ISO or UDF type. Info: https://www.garykessler.net/library/file_sigs.html
   297  func verifyHeader(buf []byte) bool {
   298  	// ISO verification - search for CD001(43 44 30 30 31) in specific file places.
   299  	// This signature usually occurs at byte offset 32769 (0x8001),
   300  	// 34817 (0x8801), or 36865 (0x9001).
   301  	// UDF verification - search for BEA01(42 45 41 30 31) in specific file places.
   302  	// This signature usually occurs at byte offset 32769 (0x8001),
   303  	// 34817 (0x8801), or 36865 (0x9001).
   304  
   305  	return (buf[32769] == 0x43 && buf[32770] == 0x44 &&
   306  		buf[32771] == 0x30 && buf[32772] == 0x30 && buf[32773] == 0x31) ||
   307  		(buf[34817] == 0x43 && buf[34818] == 0x44 &&
   308  			buf[34819] == 0x30 && buf[34820] == 0x30 && buf[34821] == 0x31) ||
   309  		(buf[36865] == 0x43 && buf[36866] == 0x44 &&
   310  			buf[36867] == 0x30 && buf[36868] == 0x30 && buf[36869] == 0x31) ||
   311  		(buf[32769] == 0x42 && buf[32770] == 0x45 &&
   312  			buf[32771] == 0x41 && buf[32772] == 0x30 && buf[32773] == 0x31) ||
   313  		(buf[34817] == 0x42 && buf[34818] == 0x45 &&
   314  			buf[34819] == 41 && buf[34820] == 0x30 && buf[34821] == 0x31) ||
   315  		(buf[36865] == 42 && buf[36866] == 45 &&
   316  			buf[36867] == 41 && buf[36868] == 0x30 && buf[36869] == 0x31)
   317  
   318  }
   319  
   320  // Reference for API usage http://pubs.vmware.com/vcloud-api-1-5/wwhelp/wwhimpl/js/html/wwhelp.htm#href=api_prog/GUID-9356B99B-E414-474A-853C-1411692AF84C.html
   321  // http://pubs.vmware.com/vcloud-api-1-5/wwhelp/wwhimpl/js/html/wwhelp.htm#href=api_prog/GUID-43DFF30E-391F-42DC-87B3-5923ABCEB366.html
   322  func getExistingMedia(vdc *Vdc) ([]*types.MediaRecordType, error) {
   323  	util.Logger.Printf("[TRACE] Querying medias \n")
   324  
   325  	mediaResults, err := queryMediaWithFilter(vdc, "vdc=="+url.QueryEscape(vdc.Vdc.HREF))
   326  
   327  	util.Logger.Printf("[TRACE] Found media records: %d \n", len(mediaResults))
   328  	return mediaResults, err
   329  }
   330  
   331  func queryMediaWithFilter(vdc *Vdc, filter string) ([]*types.MediaRecordType, error) {
   332  	typeMedia := "media"
   333  	if vdc.client.IsSysAdmin {
   334  		typeMedia = "adminMedia"
   335  	}
   336  
   337  	results, err := vdc.QueryWithNotEncodedParams(nil, map[string]string{"type": typeMedia, "filter": filter, "filterEncoded": "true"})
   338  	if err != nil {
   339  		return nil, fmt.Errorf("error querying medias %s", err)
   340  	}
   341  
   342  	mediaResults := results.Results.MediaRecord
   343  	if vdc.client.IsSysAdmin {
   344  		mediaResults = results.Results.AdminMediaRecord
   345  	}
   346  	return mediaResults, nil
   347  }
   348  
   349  // Looks for media and, if found, will delete it.
   350  // Deprecated: Use catalog.RemoveMediaIfExist
   351  func RemoveMediaImageIfExists(vdc Vdc, mediaName string) error {
   352  	mediaItem, err := vdc.FindMediaImage(mediaName)
   353  	if err == nil && mediaItem != (MediaItem{}) {
   354  		task, err := mediaItem.Delete()
   355  		if err != nil {
   356  			return fmt.Errorf("error deleting media [phase 1] %s", mediaName)
   357  		}
   358  		err = task.WaitTaskCompletion()
   359  		if err != nil {
   360  			return fmt.Errorf("error deleting media [task] %s", mediaName)
   361  		}
   362  	} else {
   363  		util.Logger.Printf("[TRACE] Media not found or error: %s - %#v \n", err, mediaItem)
   364  	}
   365  	return nil
   366  }
   367  
   368  // Looks for media and, if found, will delete it.
   369  func (adminCatalog *AdminCatalog) RemoveMediaIfExists(mediaName string) error {
   370  	media, err := adminCatalog.GetMediaByName(mediaName, true)
   371  	if err == nil {
   372  		task, err := media.Delete()
   373  		if err != nil {
   374  			return fmt.Errorf("error deleting media [phase 1] %s", mediaName)
   375  		}
   376  		err = task.WaitTaskCompletion()
   377  		if err != nil {
   378  			return fmt.Errorf("error deleting media [task] %s", mediaName)
   379  		}
   380  	} else {
   381  		util.Logger.Printf("[TRACE] Media not found or error: %s - %#v \n", err, media)
   382  	}
   383  	return nil
   384  }
   385  
   386  // Deletes the Media Item, returning an error if the vCD call fails.
   387  // Link to API call: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/DELETE-Media.html
   388  // Deprecated: Use MediaRecord.Delete
   389  func (mediaItem *MediaItem) Delete() (Task, error) {
   390  	util.Logger.Printf("[TRACE] Deleting media item: %#v", mediaItem.MediaItem.Name)
   391  
   392  	// Return the task
   393  	return mediaItem.vdc.client.ExecuteTaskRequest(mediaItem.MediaItem.HREF, http.MethodDelete,
   394  		"", "error deleting Media item: %s", nil)
   395  }
   396  
   397  // Deletes the Media Item, returning an error if the vCD call fails.
   398  // Link to API call: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/DELETE-Media.html
   399  func (media *Media) Delete() (Task, error) {
   400  	util.Logger.Printf("[TRACE] Deleting media item: %#v", media.Media.Name)
   401  
   402  	// Return the task
   403  	return media.client.ExecuteTaskRequest(media.Media.HREF, http.MethodDelete,
   404  		"", "error deleting Media item: %s", nil)
   405  }
   406  
   407  // Finds media in catalog and returns catalog item
   408  // Deprecated: Use catalog.GetMediaByName()
   409  func FindMediaAsCatalogItem(org *Org, catalogName, mediaName string) (CatalogItem, error) {
   410  	if catalogName == "" {
   411  		return CatalogItem{}, errors.New("catalog name is empty")
   412  	}
   413  	if mediaName == "" {
   414  		return CatalogItem{}, errors.New("media name is empty")
   415  	}
   416  
   417  	catalog, err := org.FindCatalog(catalogName)
   418  	if err != nil || catalog == (Catalog{}) {
   419  		return CatalogItem{}, fmt.Errorf("catalog not found or error %s", err)
   420  	}
   421  
   422  	media, err := catalog.FindCatalogItem(mediaName)
   423  	if err != nil || media == (CatalogItem{}) {
   424  		return CatalogItem{}, fmt.Errorf("media not found or error %s", err)
   425  	}
   426  	return media, nil
   427  }
   428  
   429  // Refresh refreshes the media item information by href
   430  // Deprecated: Use MediaRecord.Refresh
   431  func (mediaItem *MediaItem) Refresh() error {
   432  
   433  	if mediaItem.MediaItem == nil {
   434  		return fmt.Errorf("cannot refresh, Object is empty")
   435  	}
   436  
   437  	if mediaItem.MediaItem.Name == "nil" {
   438  		return fmt.Errorf("cannot refresh, Name is empty")
   439  	}
   440  
   441  	latestMediaItem, err := mediaItem.vdc.FindMediaImage(mediaItem.MediaItem.Name)
   442  	*mediaItem = latestMediaItem
   443  
   444  	return err
   445  }
   446  
   447  // Refresh refreshes the media information by href
   448  func (media *Media) Refresh() error {
   449  
   450  	if media.Media == nil {
   451  		return fmt.Errorf("cannot refresh, Object is empty")
   452  	}
   453  
   454  	url := media.Media.HREF
   455  
   456  	// Empty struct before a new unmarshal, otherwise we end up with duplicate
   457  	// elements in slices.
   458  	media.Media = &types.Media{}
   459  
   460  	_, err := media.client.ExecuteRequest(url, http.MethodGet,
   461  		"", "error retrieving media: %s", nil, media.Media)
   462  
   463  	return err
   464  }
   465  
   466  // GetMediaByHref finds a Media by HREF
   467  // On success, returns a pointer to the Media structure and a nil error
   468  // On failure, returns a nil pointer and an error
   469  func (cat *Catalog) GetMediaByHref(mediaHref string) (*Media, error) {
   470  
   471  	media := NewMedia(cat.client)
   472  
   473  	_, err := cat.client.ExecuteRequest(mediaHref, http.MethodGet,
   474  		"", "error retrieving media: %#v", nil, media.Media)
   475  	if err != nil && strings.Contains(err.Error(), "MajorErrorCode:403") {
   476  		return nil, ErrorEntityNotFound
   477  	}
   478  	if err != nil {
   479  		return nil, err
   480  	}
   481  	return media, nil
   482  }
   483  
   484  // GetMediaByName finds a Media by Name
   485  // On success, returns a pointer to the Media structure and a nil error
   486  // On failure, returns a nil pointer and an error
   487  func (cat *Catalog) GetMediaByName(mediaName string, refresh bool) (*Media, error) {
   488  	if refresh {
   489  		err := cat.Refresh()
   490  		if err != nil {
   491  			return nil, err
   492  		}
   493  	}
   494  	for _, catalogItems := range cat.Catalog.CatalogItems {
   495  		for _, catalogItem := range catalogItems.CatalogItem {
   496  			if catalogItem.Name == mediaName && catalogItem.Type == "application/vnd.vmware.vcloud.catalogItem+xml" {
   497  				catalogItemElement, err := cat.GetCatalogItemByHref(catalogItem.HREF)
   498  				if err != nil {
   499  					return nil, err
   500  				}
   501  				return cat.GetMediaByHref(catalogItemElement.CatalogItem.Entity.HREF)
   502  			}
   503  		}
   504  	}
   505  	return nil, ErrorEntityNotFound
   506  }
   507  
   508  // GetMediaById finds a Media by ID
   509  // On success, returns a pointer to the Media structure and a nil error
   510  // On failure, returns a nil pointer and an error
   511  func (catalog *Catalog) GetMediaById(mediaId string) (*Media, error) {
   512  	typeMedia := "media"
   513  	if catalog.client.IsSysAdmin {
   514  		typeMedia = "adminMedia"
   515  	}
   516  
   517  	results, err := catalog.client.QueryWithNotEncodedParams(nil, map[string]string{"type": typeMedia,
   518  		"filter":        fmt.Sprintf("catalogName==%s", url.QueryEscape(catalog.Catalog.Name)),
   519  		"filterEncoded": "true"})
   520  	if err != nil {
   521  		return nil, fmt.Errorf("error querying medias %s", err)
   522  	}
   523  
   524  	mediaResults := results.Results.MediaRecord
   525  	if catalog.client.IsSysAdmin {
   526  		mediaResults = results.Results.AdminMediaRecord
   527  	}
   528  	for _, mediaRecord := range mediaResults {
   529  		if equalIds(mediaId, mediaRecord.ID, mediaRecord.HREF) {
   530  			return catalog.GetMediaByHref(mediaRecord.HREF)
   531  		}
   532  	}
   533  	return nil, ErrorEntityNotFound
   534  }
   535  
   536  // GetMediaByNameOrId finds a Media by Name or ID
   537  // On success, returns a pointer to the Media structure and a nil error
   538  // On failure, returns a nil pointer and an error
   539  func (cat *Catalog) GetMediaByNameOrId(identifier string, refresh bool) (*Media, error) {
   540  	getByName := func(name string, refresh bool) (interface{}, error) { return cat.GetMediaByName(name, refresh) }
   541  	getById := func(id string, refresh bool) (interface{}, error) { return cat.GetMediaById(id) }
   542  	entity, err := getEntityByNameOrId(getByName, getById, identifier, refresh)
   543  	if entity == nil {
   544  		return nil, err
   545  	}
   546  	return entity.(*Media), err
   547  }
   548  
   549  // GetMediaByHref finds a Media by HREF
   550  // On success, returns a pointer to the Media structure and a nil error
   551  // On failure, returns a nil pointer and an error
   552  func (adminCatalog *AdminCatalog) GetMediaByHref(mediaHref string) (*Media, error) {
   553  	catalog := NewCatalog(adminCatalog.client)
   554  	catalog.Catalog = &adminCatalog.AdminCatalog.Catalog
   555  	catalog.parent = adminCatalog.parent
   556  	return catalog.GetMediaByHref(mediaHref)
   557  }
   558  
   559  // GetMediaByName finds a Media by Name
   560  // On success, returns a pointer to the Media structure and a nil error
   561  // On failure, returns a nil pointer and an error
   562  func (adminCatalog *AdminCatalog) GetMediaByName(mediaName string, refresh bool) (*Media, error) {
   563  	catalog := NewCatalog(adminCatalog.client)
   564  	catalog.Catalog = &adminCatalog.AdminCatalog.Catalog
   565  	catalog.parent = adminCatalog.parent
   566  	return catalog.GetMediaByName(mediaName, refresh)
   567  }
   568  
   569  // GetMediaById finds a Media by ID
   570  // On success, returns a pointer to the Media structure and a nil error
   571  // On failure, returns a nil pointer and an error
   572  func (adminCatalog *AdminCatalog) GetMediaById(mediaId string) (*Media, error) {
   573  	catalog := NewCatalog(adminCatalog.client)
   574  	catalog.Catalog = &adminCatalog.AdminCatalog.Catalog
   575  	catalog.parent = adminCatalog.parent
   576  	return catalog.GetMediaById(mediaId)
   577  }
   578  
   579  // GetMediaByNameOrId finds a Media by Name or ID
   580  // On success, returns a pointer to the Media structure and a nil error
   581  // On failure, returns a nil pointer and an error
   582  func (adminCatalog *AdminCatalog) GetMediaByNameOrId(identifier string, refresh bool) (*Media, error) {
   583  	catalog := NewCatalog(adminCatalog.client)
   584  	catalog.Catalog = &adminCatalog.AdminCatalog.Catalog
   585  	catalog.parent = adminCatalog.parent
   586  	return catalog.GetMediaByNameOrId(identifier, refresh)
   587  }
   588  
   589  // QueryMedia returns media image found in system using `name` and `catalog name` as query.
   590  func (catalog *Catalog) QueryMedia(mediaName string) (*MediaRecord, error) {
   591  	util.Logger.Printf("[TRACE] Querying medias by name and catalog\n")
   592  
   593  	if catalog == nil || catalog.Catalog == nil || catalog.Catalog.Name == "" {
   594  		return nil, errors.New("catalog is empty")
   595  	}
   596  	if mediaName == "" {
   597  		return nil, errors.New("media name is empty")
   598  	}
   599  
   600  	typeMedia := "media"
   601  	if catalog.client.IsSysAdmin {
   602  		typeMedia = "adminMedia"
   603  	}
   604  
   605  	results, err := catalog.client.QueryWithNotEncodedParams(nil, map[string]string{"type": typeMedia,
   606  		"filter": fmt.Sprintf("name==%s;catalogName==%s",
   607  			url.QueryEscape(mediaName),
   608  			url.QueryEscape(catalog.Catalog.Name)),
   609  		"filterEncoded": "true"})
   610  	if err != nil {
   611  		return nil, fmt.Errorf("error querying medias %s", err)
   612  	}
   613  	newMediaRecord := NewMediaRecord(catalog.client)
   614  
   615  	mediaResults := results.Results.MediaRecord
   616  	if catalog.client.IsSysAdmin {
   617  		mediaResults = results.Results.AdminMediaRecord
   618  	}
   619  	if len(mediaResults) == 1 {
   620  		newMediaRecord.MediaRecord = mediaResults[0]
   621  	}
   622  
   623  	if len(mediaResults) == 0 {
   624  		return nil, ErrorEntityNotFound
   625  	}
   626  	// this shouldn't happen, but we will check anyways
   627  	if len(mediaResults) > 1 {
   628  		return nil, fmt.Errorf("found more than one result %#v with catalog name %s and media name %s ", mediaResults, catalog.Catalog.Name, mediaName)
   629  	}
   630  
   631  	util.Logger.Printf("[TRACE] Found media record by name: %#v \n", mediaResults[0])
   632  	return newMediaRecord, nil
   633  }
   634  
   635  // QueryMedia returns media image found in system using `name` and `catalog name` as query.
   636  func (adminCatalog *AdminCatalog) QueryMedia(mediaName string) (*MediaRecord, error) {
   637  	catalog := NewCatalog(adminCatalog.client)
   638  	catalog.Catalog = &adminCatalog.AdminCatalog.Catalog
   639  	catalog.parent = adminCatalog.parent
   640  	return catalog.QueryMedia(mediaName)
   641  }
   642  
   643  // QueryMediaById returns a MediaRecord associated to the given media item URN. Returns ErrorEntityNotFound
   644  // if it is not found, or an error if there's more than one result.
   645  func (vcdClient *VCDClient) QueryMediaById(mediaId string) (*MediaRecord, error) {
   646  	if mediaId == "" {
   647  		return nil, fmt.Errorf("media ID is empty")
   648  	}
   649  
   650  	filterType := types.QtMedia
   651  	if vcdClient.Client.IsSysAdmin {
   652  		filterType = types.QtAdminMedia
   653  	}
   654  	results, err := vcdClient.Client.QueryWithNotEncodedParams(nil, map[string]string{
   655  		"type":          filterType,
   656  		"filter":        fmt.Sprintf("id==%s", url.QueryEscape(mediaId)),
   657  		"filterEncoded": "true"})
   658  	if err != nil {
   659  		return nil, fmt.Errorf("error querying medias %s", err)
   660  	}
   661  	newMediaRecord := NewMediaRecord(&vcdClient.Client)
   662  
   663  	mediaResults := results.Results.MediaRecord
   664  	if vcdClient.Client.IsSysAdmin {
   665  		mediaResults = results.Results.AdminMediaRecord
   666  	}
   667  
   668  	if len(mediaResults) == 0 {
   669  		return nil, ErrorEntityNotFound
   670  	}
   671  	if len(mediaResults) > 1 {
   672  		return nil, fmt.Errorf("found %#v results with media ID %s", len(mediaResults), mediaId)
   673  	}
   674  
   675  	newMediaRecord.MediaRecord = mediaResults[0]
   676  	return newMediaRecord, nil
   677  }
   678  
   679  // Refresh refreshes the media information by href
   680  func (mediaRecord *MediaRecord) Refresh() error {
   681  
   682  	if mediaRecord.MediaRecord == nil {
   683  		return fmt.Errorf("cannot refresh, Object is empty")
   684  	}
   685  
   686  	if mediaRecord.MediaRecord.Name == "" {
   687  		return fmt.Errorf("cannot refresh, Name is empty")
   688  	}
   689  
   690  	url := mediaRecord.MediaRecord.HREF
   691  
   692  	// Empty struct before a new unmarshal, otherwise we end up with duplicate
   693  	// elements in slices.
   694  	mediaRecord.MediaRecord = &types.MediaRecordType{}
   695  
   696  	_, err := mediaRecord.client.ExecuteRequest(url, http.MethodGet,
   697  		"", "error retrieving media: %s", nil, mediaRecord.MediaRecord)
   698  
   699  	return err
   700  }
   701  
   702  // Deletes the Media Item, returning an error if the vCD call fails.
   703  // Link to API call: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/DELETE-Media.html
   704  func (mediaRecord *MediaRecord) Delete() (Task, error) {
   705  	util.Logger.Printf("[TRACE] Deleting media item: %#v", mediaRecord.MediaRecord.Name)
   706  
   707  	// Return the task
   708  	return mediaRecord.client.ExecuteTaskRequest(mediaRecord.MediaRecord.HREF, http.MethodDelete,
   709  		"", "error deleting Media item: %s", nil)
   710  }
   711  
   712  // QueryAllMedia returns all media images found in system using `name` as query.
   713  func (vdc *Vdc) QueryAllMedia(mediaName string) ([]*MediaRecord, error) {
   714  	util.Logger.Printf("[TRACE] Querying medias by name\n")
   715  
   716  	if mediaName == "" {
   717  		return nil, errors.New("media name is empty")
   718  	}
   719  
   720  	typeMedia := "media"
   721  	if vdc.client.IsSysAdmin {
   722  		typeMedia = "adminMedia"
   723  	}
   724  
   725  	results, err := vdc.client.QueryWithNotEncodedParams(nil, map[string]string{"type": typeMedia,
   726  		"filter": fmt.Sprintf("name==%s", url.QueryEscape(mediaName))})
   727  	if err != nil {
   728  		return nil, fmt.Errorf("error querying medias %s", err)
   729  	}
   730  
   731  	mediaResults := results.Results.MediaRecord
   732  	if vdc.client.IsSysAdmin {
   733  		mediaResults = results.Results.AdminMediaRecord
   734  	}
   735  
   736  	if len(mediaResults) == 0 {
   737  		return nil, ErrorEntityNotFound
   738  	}
   739  
   740  	var newMediaRecords []*MediaRecord
   741  	for _, mediaResult := range mediaResults {
   742  		newMediaRecord := NewMediaRecord(vdc.client)
   743  		newMediaRecord.MediaRecord = mediaResult
   744  		newMediaRecords = append(newMediaRecords, newMediaRecord)
   745  	}
   746  
   747  	util.Logger.Printf("[TRACE] Found media records by name: %#v \n", mediaResults)
   748  	return newMediaRecords, nil
   749  }
   750  
   751  // enableDownload prepares a media item for download and returns a download link
   752  // Note: depending on the size of the item, it may take a long time.
   753  func (media *Media) enableDownload() (string, error) {
   754  	downloadUrl := getUrlFromLink(media.Media.Link, "enable", "")
   755  	if downloadUrl == "" {
   756  		return "", fmt.Errorf("no enable URL found")
   757  	}
   758  	// The result of this operation is the creation of an entry in the 'Files' field of the media structure
   759  	// Inside that field, there will be a Link entry with the URL for the download
   760  	// e.g.
   761  	//<Files>
   762  	//    <File size="25434" name="file">
   763  	//        <Link rel="download:default" href="https://example.com/transfer/1638969a-06da-4f6c-b097-7796c1556c54/file"/>
   764  	//    </File>
   765  	//</Files>
   766  	task, err := media.client.executeTaskRequest(
   767  		downloadUrl,
   768  		http.MethodPost,
   769  		types.MimeTask,
   770  		"error enabling download: %s",
   771  		nil,
   772  		media.client.APIVersion)
   773  	if err != nil {
   774  		return "", err
   775  	}
   776  	err = task.WaitTaskCompletion()
   777  	if err != nil {
   778  		return "", err
   779  	}
   780  
   781  	err = media.Refresh()
   782  	if err != nil {
   783  		return "", err
   784  	}
   785  
   786  	if media.Media.Files == nil || len(media.Media.Files.File) == 0 {
   787  		return "", fmt.Errorf("no downloadable file info found")
   788  	}
   789  	downloadHref := ""
   790  	for _, f := range media.Media.Files.File {
   791  		for _, l := range f.Link {
   792  			if l.Rel == "download:default" {
   793  				downloadHref = l.HREF
   794  				break
   795  			}
   796  			if downloadHref != "" {
   797  				break
   798  			}
   799  		}
   800  	}
   801  
   802  	if downloadHref == "" {
   803  		return "", fmt.Errorf("no download URL found")
   804  	}
   805  
   806  	return downloadHref, nil
   807  }
   808  
   809  // Download gets the contents of a media item as a byte stream
   810  // NOTE: the whole item will be saved in local memory. Do not attempt this operation for very large items
   811  func (media *Media) Download() ([]byte, error) {
   812  
   813  	downloadHref, err := media.enableDownload()
   814  	if err != nil {
   815  		return nil, err
   816  	}
   817  
   818  	downloadUrl, err := url.ParseRequestURI(downloadHref)
   819  	if err != nil {
   820  		return nil, fmt.Errorf("error getting download URL: %s", err)
   821  	}
   822  
   823  	request := media.client.NewRequest(map[string]string{}, http.MethodGet, *downloadUrl, nil)
   824  	resp, err := media.client.Http.Do(request)
   825  	if err != nil {
   826  		return nil, fmt.Errorf("error getting media download: %s", err)
   827  	}
   828  
   829  	if !isSuccessStatus(resp.StatusCode) {
   830  		return nil, fmt.Errorf("error downloading media: %s", resp.Status)
   831  	}
   832  	body, err := io.ReadAll(resp.Body)
   833  
   834  	defer func() {
   835  		err = resp.Body.Close()
   836  		if err != nil {
   837  			panic(fmt.Sprintf("error closing body: %s", err))
   838  		}
   839  	}()
   840  
   841  	if err != nil {
   842  		return nil, err
   843  	}
   844  	return body, nil
   845  }