Actions Artifacts support uploading multiple files and directories (#24874)
current actions artifacts implementation only support single file artifact. To support multiple files uploading, it needs: - save each file to each db record with same run-id, same artifact-name and proper artifact-path - need change artifact uploading url without artifact-id, multiple files creates multiple artifact-ids - support `path` in download-artifact action. artifact should download to `{path}/{artifact-path}`. - in repo action view, it provides zip download link in artifacts list in summary page, no matter this artifact contains single or multiple files.
This commit is contained in:
parent
3acaaa29dd
commit
f3d293d2bb
10 changed files with 644 additions and 354 deletions
|
@ -13,18 +13,18 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestActionsArtifactUpload(t *testing.T) {
|
||||
type uploadArtifactResponse struct {
|
||||
FileContainerResourceURL string `json:"fileContainerResourceUrl"`
|
||||
}
|
||||
|
||||
type getUploadArtifactRequest struct {
|
||||
Type string
|
||||
Name string
|
||||
}
|
||||
|
||||
func TestActionsArtifactUploadSingleFile(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
type uploadArtifactResponse struct {
|
||||
FileContainerResourceURL string `json:"fileContainerResourceUrl"`
|
||||
}
|
||||
|
||||
type getUploadArtifactRequest struct {
|
||||
Type string
|
||||
Name string
|
||||
}
|
||||
|
||||
// acquire artifact upload url
|
||||
req := NewRequestWithJSON(t, "POST", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts", getUploadArtifactRequest{
|
||||
Type: "actions_storage",
|
||||
|
@ -52,32 +52,33 @@ func TestActionsArtifactUpload(t *testing.T) {
|
|||
t.Logf("Create artifact confirm")
|
||||
|
||||
// confirm artifact upload
|
||||
req = NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
|
||||
req = NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts?artifactName=artifact")
|
||||
req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestActionsArtifactUploadNotExist(t *testing.T) {
|
||||
func TestActionsArtifactUploadInvalidHash(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
// artifact id 54321 not exist
|
||||
url := "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts/54321/upload?itemPath=artifact/abc.txt"
|
||||
url := "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts/8e5b948a454515dbabfc7eb718ddddddd/upload?itemPath=artifact/abc.txt"
|
||||
body := strings.Repeat("A", 1024)
|
||||
req := NewRequestWithBody(t, "PUT", url, strings.NewReader(body))
|
||||
req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
req.Header.Add("Content-Range", "bytes 0-1023/1024")
|
||||
req.Header.Add("x-tfs-filelength", "1024")
|
||||
req.Header.Add("x-actions-results-md5", "1HsSe8LeLWh93ILaw1TEFQ==") // base64(md5(body))
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
resp := MakeRequest(t, req, http.StatusBadRequest)
|
||||
assert.Contains(t, resp.Body.String(), "Invalid artifact hash")
|
||||
}
|
||||
|
||||
func TestActionsArtifactConfirmUpload(t *testing.T) {
|
||||
func TestActionsArtifactConfirmUploadWithoutName(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
req := NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
|
||||
req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Contains(t, resp.Body.String(), "success")
|
||||
resp := MakeRequest(t, req, http.StatusBadRequest)
|
||||
assert.Contains(t, resp.Body.String(), "artifact name is empty")
|
||||
}
|
||||
|
||||
func TestActionsArtifactUploadWithoutToken(t *testing.T) {
|
||||
|
@ -87,20 +88,28 @@ func TestActionsArtifactUploadWithoutToken(t *testing.T) {
|
|||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
type (
|
||||
listArtifactsResponseItem struct {
|
||||
Name string `json:"name"`
|
||||
FileContainerResourceURL string `json:"fileContainerResourceUrl"`
|
||||
}
|
||||
listArtifactsResponse struct {
|
||||
Count int64 `json:"count"`
|
||||
Value []listArtifactsResponseItem `json:"value"`
|
||||
}
|
||||
downloadArtifactResponseItem struct {
|
||||
Path string `json:"path"`
|
||||
ItemType string `json:"itemType"`
|
||||
ContentLocation string `json:"contentLocation"`
|
||||
}
|
||||
downloadArtifactResponse struct {
|
||||
Value []downloadArtifactResponseItem `json:"value"`
|
||||
}
|
||||
)
|
||||
|
||||
func TestActionsArtifactDownload(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
type (
|
||||
listArtifactsResponseItem struct {
|
||||
Name string `json:"name"`
|
||||
FileContainerResourceURL string `json:"fileContainerResourceUrl"`
|
||||
}
|
||||
listArtifactsResponse struct {
|
||||
Count int64 `json:"count"`
|
||||
Value []listArtifactsResponseItem `json:"value"`
|
||||
}
|
||||
)
|
||||
|
||||
req := NewRequest(t, "GET", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
|
||||
req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
@ -110,19 +119,8 @@ func TestActionsArtifactDownload(t *testing.T) {
|
|||
assert.Equal(t, "artifact", listResp.Value[0].Name)
|
||||
assert.Contains(t, listResp.Value[0].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
|
||||
|
||||
type (
|
||||
downloadArtifactResponseItem struct {
|
||||
Path string `json:"path"`
|
||||
ItemType string `json:"itemType"`
|
||||
ContentLocation string `json:"contentLocation"`
|
||||
}
|
||||
downloadArtifactResponse struct {
|
||||
Value []downloadArtifactResponseItem `json:"value"`
|
||||
}
|
||||
)
|
||||
|
||||
idx := strings.Index(listResp.Value[0].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
|
||||
url := listResp.Value[0].FileContainerResourceURL[idx+1:]
|
||||
url := listResp.Value[0].FileContainerResourceURL[idx+1:] + "?itemPath=artifact"
|
||||
req = NewRequest(t, "GET", url)
|
||||
req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
@ -141,3 +139,116 @@ func TestActionsArtifactDownload(t *testing.T) {
|
|||
body := strings.Repeat("A", 1024)
|
||||
assert.Equal(t, resp.Body.String(), body)
|
||||
}
|
||||
|
||||
func TestActionsArtifactUploadMultipleFile(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
const testArtifactName = "multi-files"
|
||||
|
||||
// acquire artifact upload url
|
||||
req := NewRequestWithJSON(t, "POST", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts", getUploadArtifactRequest{
|
||||
Type: "actions_storage",
|
||||
Name: testArtifactName,
|
||||
})
|
||||
req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var uploadResp uploadArtifactResponse
|
||||
DecodeJSON(t, resp, &uploadResp)
|
||||
assert.Contains(t, uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
|
||||
|
||||
type uploadingFile struct {
|
||||
Path string
|
||||
Content string
|
||||
MD5 string
|
||||
}
|
||||
|
||||
files := []uploadingFile{
|
||||
{
|
||||
Path: "abc.txt",
|
||||
Content: strings.Repeat("A", 1024),
|
||||
MD5: "1HsSe8LeLWh93ILaw1TEFQ==",
|
||||
},
|
||||
{
|
||||
Path: "xyz/def.txt",
|
||||
Content: strings.Repeat("B", 1024),
|
||||
MD5: "6fgADK/7zjadf+6cB9Q1CQ==",
|
||||
},
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
// get upload url
|
||||
idx := strings.Index(uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
|
||||
url := uploadResp.FileContainerResourceURL[idx:] + "?itemPath=" + testArtifactName + "/" + f.Path
|
||||
|
||||
// upload artifact chunk
|
||||
req = NewRequestWithBody(t, "PUT", url, strings.NewReader(f.Content))
|
||||
req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
req.Header.Add("Content-Range", "bytes 0-1023/1024")
|
||||
req.Header.Add("x-tfs-filelength", "1024")
|
||||
req.Header.Add("x-actions-results-md5", f.MD5) // base64(md5(body))
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
t.Logf("Create artifact confirm")
|
||||
|
||||
// confirm artifact upload
|
||||
req = NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts?artifactName="+testArtifactName)
|
||||
req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestActionsArtifactDownloadMultiFiles(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
const testArtifactName = "multi-files"
|
||||
|
||||
req := NewRequest(t, "GET", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
|
||||
req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var listResp listArtifactsResponse
|
||||
DecodeJSON(t, resp, &listResp)
|
||||
assert.Equal(t, int64(2), listResp.Count)
|
||||
|
||||
var fileContainerResourceURL string
|
||||
for _, v := range listResp.Value {
|
||||
if v.Name == testArtifactName {
|
||||
fileContainerResourceURL = v.FileContainerResourceURL
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.Contains(t, fileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
|
||||
|
||||
idx := strings.Index(fileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
|
||||
url := fileContainerResourceURL[idx+1:] + "?itemPath=" + testArtifactName
|
||||
req = NewRequest(t, "GET", url)
|
||||
req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
var downloadResp downloadArtifactResponse
|
||||
DecodeJSON(t, resp, &downloadResp)
|
||||
assert.Len(t, downloadResp.Value, 2)
|
||||
|
||||
downloads := [][]string{{"multi-files/abc.txt", "A"}, {"multi-files/xyz/def.txt", "B"}}
|
||||
for _, v := range downloadResp.Value {
|
||||
var bodyChar string
|
||||
var path string
|
||||
for _, d := range downloads {
|
||||
if v.Path == d[0] {
|
||||
path = d[0]
|
||||
bodyChar = d[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
value := v
|
||||
assert.Equal(t, path, value.Path)
|
||||
assert.Equal(t, "file", value.ItemType)
|
||||
assert.Contains(t, value.ContentLocation, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
|
||||
|
||||
idx = strings.Index(value.ContentLocation, "/api/actions_pipeline/_apis/pipelines/")
|
||||
url = value.ContentLocation[idx:]
|
||||
req = NewRequest(t, "GET", url)
|
||||
req = addTokenAuthHeader(req, "Bearer 8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
body := strings.Repeat(bodyChar, 1024)
|
||||
assert.Equal(t, resp.Body.String(), body)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue