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 }