Skip to content

Commit df27540

Browse files
authored
Media item upload any file and download (#622)
* Add method Catalog.UploadMediaFile * Add changelog item * Add method Media.Download It allows to download the media contents as a bytes stream * Add test to upload and download media item * Add deferred close for response body --------- Signed-off-by: Giuseppe Maxia <gmaxia@vmware.com>
1 parent db139b2 commit df27540

File tree

5 files changed

+185
-9
lines changed

5 files changed

+185
-9
lines changed

.changes/v2.22.0/621-features.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* Added method `Catalog.UploadMediaFile` to upload any file as catalog Media [GH-621,GH-622]

.changes/v2.22.0/622-features.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* Added method `Media.Download` to download a Media item as a byte stream [GH-622]

govcd/catalog.go

+17-8
Original file line numberDiff line numberDiff line change
@@ -827,7 +827,14 @@ func removeCatalogItemOnError(client *Client, vappTemplateLink *url.URL, itemNam
827827
}
828828
}
829829

830+
// UploadMediaImage uploads a media image to the catalog
830831
func (cat *Catalog) UploadMediaImage(mediaName, mediaDescription, filePath string, uploadPieceSize int64) (UploadTask, error) {
832+
return cat.UploadMediaFile(mediaName, mediaDescription, filePath, uploadPieceSize, true)
833+
}
834+
835+
// UploadMediaFile uploads any file to the catalog.
836+
// However, if checkFileIsIso is true, only .ISO are allowed.
837+
func (cat *Catalog) UploadMediaFile(fileName, mediaDescription, filePath string, uploadPieceSize int64, checkFileIsIso bool) (UploadTask, error) {
831838

832839
if *cat == (Catalog{}) {
833840
return UploadTask{}, errors.New("catalog can not be empty or nil")
@@ -838,9 +845,11 @@ func (cat *Catalog) UploadMediaImage(mediaName, mediaDescription, filePath strin
838845
return UploadTask{}, err
839846
}
840847

841-
isISOGood, err := verifyIso(mediaFilePath)
842-
if err != nil || !isISOGood {
843-
return UploadTask{}, fmt.Errorf("[ERROR] File %s isn't correct iso file: %#v", mediaFilePath, err)
848+
if checkFileIsIso {
849+
isISOGood, err := verifyIso(mediaFilePath)
850+
if err != nil || !isISOGood {
851+
return UploadTask{}, fmt.Errorf("[ERROR] File %s isn't correct iso file: %#v", mediaFilePath, err)
852+
}
844853
}
845854

846855
file, e := os.Stat(mediaFilePath)
@@ -850,8 +859,8 @@ func (cat *Catalog) UploadMediaImage(mediaName, mediaDescription, filePath strin
850859
fileSize := file.Size()
851860

852861
for _, catalogItemName := range getExistingCatalogItems(cat) {
853-
if catalogItemName == mediaName {
854-
return UploadTask{}, fmt.Errorf("media item '%s' already exists. Upload with different name", mediaName)
862+
if catalogItemName == fileName {
863+
return UploadTask{}, fmt.Errorf("media item '%s' already exists. Upload with different name", fileName)
855864
}
856865
}
857866

@@ -860,17 +869,17 @@ func (cat *Catalog) UploadMediaImage(mediaName, mediaDescription, filePath strin
860869
return UploadTask{}, err
861870
}
862871

863-
media, err := createMedia(cat.client, catalogItemUploadURL.String(), mediaName, mediaDescription, fileSize)
872+
media, err := createMedia(cat.client, catalogItemUploadURL.String(), fileName, mediaDescription, fileSize)
864873
if err != nil {
865874
return UploadTask{}, fmt.Errorf("[ERROR] Issue creating media: %#v", err)
866875
}
867876

868-
createdMedia, err := queryMedia(cat.client, media.Entity.HREF, mediaName)
877+
createdMedia, err := queryMedia(cat.client, media.Entity.HREF, fileName)
869878
if err != nil {
870879
return UploadTask{}, err
871880
}
872881

873-
return executeUpload(cat.client, createdMedia, mediaFilePath, mediaName, fileSize, uploadPieceSize)
882+
return executeUpload(cat.client, createdMedia, mediaFilePath, fileName, fileSize, uploadPieceSize)
874883
}
875884

876885
// Refresh gets a fresh copy of the catalog from vCD

govcd/media.go

+96
Original file line numberDiff line numberDiff line change
@@ -747,3 +747,99 @@ func (vdc *Vdc) QueryAllMedia(mediaName string) ([]*MediaRecord, error) {
747747
util.Logger.Printf("[TRACE] Found media records by name: %#v \n", mediaResults)
748748
return newMediaRecords, nil
749749
}
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+
}

govcd/media_test.go

+70-1
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ package govcd
88

99
import (
1010
"fmt"
11-
1211
. "gopkg.in/check.v1"
12+
"os"
13+
"path"
14+
"runtime"
1315
)
1416

1517
// Tests System function Delete by creating media item and
@@ -57,6 +59,73 @@ func (vcd *TestVCD) Test_DeleteMedia(check *C) {
5759
check.Assert(IsNotFound(err), Equals, true)
5860
}
5961

62+
func (vcd *TestVCD) Test_UploadAnyMediaFile(check *C) {
63+
fmt.Printf("Running: %s\n", check.TestName())
64+
65+
if vcd.config.VCD.Org == "" {
66+
check.Skip("Test_UploadAnyMediaFile: Org name not given")
67+
return
68+
}
69+
if vcd.config.VCD.Catalog.Name == "" {
70+
check.Skip("Test_UploadAnyMediaFile: Catalog name not given")
71+
return
72+
}
73+
org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org)
74+
check.Assert(err, IsNil)
75+
check.Assert(org, NotNil)
76+
77+
catalog, err := org.GetCatalogByName(vcd.config.VCD.Catalog.Name, false)
78+
check.Assert(err, IsNil)
79+
check.Assert(catalog, NotNil)
80+
81+
_, sourceFile, _, _ := runtime.Caller(0)
82+
sourceFile = path.Clean(sourceFile)
83+
itemName := check.TestName()
84+
itemPath := sourceFile
85+
86+
// Upload the source file of the current test as a media item
87+
uploadTask, err := catalog.UploadMediaFile(itemName, "Text file uploaded from test", itemPath, 1024, false)
88+
check.Assert(err, IsNil)
89+
err = uploadTask.WaitTaskCompletion()
90+
check.Assert(err, IsNil)
91+
92+
AddToCleanupList(itemName, "mediaCatalogImage", vcd.org.Org.Name+"|"+vcd.config.VCD.Catalog.Name, check.TestName())
93+
94+
// Retrieve the media item
95+
media, err := catalog.GetMediaByName(itemName, true)
96+
check.Assert(err, IsNil)
97+
check.Assert(media, NotNil)
98+
check.Assert(media.Media.Name, Equals, itemName)
99+
100+
// Repeat the download a few times. Make sure that a repeated download works as well as the first one
101+
for i := 0; i < 2; i++ {
102+
// Download the media item from VCD as a byte slice
103+
contents, err := media.Download()
104+
check.Assert(err, IsNil)
105+
check.Assert(len(contents), Not(Equals), 0)
106+
check.Assert(media.Media.Files, NotNil)
107+
check.Assert(media.Media.Files.File, NotNil)
108+
check.Assert(media.Media.Files.File[0].Name, Not(Equals), "")
109+
check.Assert(len(media.Media.Files.File[0].Link), Not(Equals), 0)
110+
111+
// Read the source file from disk
112+
fromFile, err := os.ReadFile(path.Clean(sourceFile))
113+
check.Assert(err, IsNil)
114+
// Make sure that what we downloaded from VCD corresponds to the file contents.
115+
check.Assert(len(fromFile), Equals, len(contents))
116+
check.Assert(fromFile, DeepEquals, contents)
117+
}
118+
119+
task, err := media.Delete()
120+
check.Assert(err, IsNil)
121+
err = task.WaitTaskCompletion()
122+
check.Assert(err, IsNil)
123+
124+
_, err = catalog.GetMediaByName(itemName, true)
125+
check.Assert(err, NotNil)
126+
check.Assert(IsNotFound(err), Equals, true)
127+
}
128+
60129
// Tests System function Refresh
61130
func (vcd *TestVCD) Test_RefreshMedia(check *C) {
62131
fmt.Printf("Running: %s\n", check.TestName())

0 commit comments

Comments
 (0)