mirror of
https://github.com/go-gitea/gitea
synced 2025-07-22 18:28:37 +00:00
Artifacts download api for artifact actions v4 (#33510)
* download endpoint has to use 302 redirect * fake blob download used if direct download not possible * downloading v3 artifacts not possible New repo apis based on GitHub Rest V3 - GET /runs/{run}/artifacts (Cannot use run index of url due to not being unique) - GET /artifacts - GET + DELETE /artifacts/{artifact_id} - GET /artifacts/{artifact_id}/zip - (GET /artifacts/{artifact_id}/zip/raw this is a workaround for a http 302 assertion in actions/toolkit) - api docs removed this is protected by a signed url like the internal artifacts api and no longer usable with any token or swagger - returns http 401 if the signature is invalid - or change the artifact id - or expired after 1 hour Closes #33353 Closes #32124 --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -292,7 +292,7 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st
|
||||
}
|
||||
|
||||
artifact.StoragePath = storagePath
|
||||
artifact.Status = int64(actions.ArtifactStatusUploadConfirmed)
|
||||
artifact.Status = actions.ArtifactStatusUploadConfirmed
|
||||
if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil {
|
||||
return fmt.Errorf("update artifact error: %v", err)
|
||||
}
|
||||
|
@@ -25,7 +25,7 @@ package actions
|
||||
// 1.3. Continue Upload Zip Content to Blobstorage (unauthenticated request), repeat until everything is uploaded
|
||||
// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=appendBlock
|
||||
// 1.4. BlockList xml payload to Blobstorage (unauthenticated request)
|
||||
// Files of about 800MB are parallel in parallel and / or out of order, this file is needed to enshure the correct order
|
||||
// Files of about 800MB are parallel in parallel and / or out of order, this file is needed to ensure the correct order
|
||||
// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=blockList
|
||||
// Request
|
||||
// <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
|
@@ -1241,6 +1241,13 @@ func Routes() *web.Router {
|
||||
}, reqToken(), reqAdmin())
|
||||
m.Group("/actions", func() {
|
||||
m.Get("/tasks", repo.ListActionTasks)
|
||||
m.Get("/runs/{run}/artifacts", repo.GetArtifactsOfRun)
|
||||
m.Get("/artifacts", repo.GetArtifacts)
|
||||
m.Group("/artifacts/{artifact_id}", func() {
|
||||
m.Get("", repo.GetArtifact)
|
||||
m.Delete("", reqRepoWriter(unit.TypeActions), repo.DeleteArtifact)
|
||||
})
|
||||
m.Get("/artifacts/{artifact_id}/zip", repo.DownloadArtifact)
|
||||
}, reqRepoReader(unit.TypeActions), context.ReferencesGitRepo(true))
|
||||
m.Group("/keys", func() {
|
||||
m.Combo("").Get(repo.ListDeployKeys).
|
||||
@@ -1401,6 +1408,10 @@ func Routes() *web.Router {
|
||||
}, repoAssignment(), checkTokenPublicOnly())
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
|
||||
|
||||
// Artifacts direct download endpoint authenticates via signed url
|
||||
// it is protected by the "sig" parameter (to help to access private repo), so no need to use other middlewares
|
||||
m.Get("/repos/{username}/{reponame}/actions/artifacts/{artifact_id}/zip/raw", repo.DownloadArtifactRaw)
|
||||
|
||||
// Notifications (requires notifications scope)
|
||||
m.Group("/repos", func() {
|
||||
m.Group("/{username}/{reponame}", func() {
|
||||
|
@@ -4,13 +4,25 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
go_context "context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
secret_model "code.gitea.io/gitea/models/secret"
|
||||
"code.gitea.io/gitea/modules/actions"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
@@ -855,3 +867,382 @@ func ActionsEnableWorkflow(ctx *context.APIContext) {
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// GetArtifacts Lists all artifacts for a repository.
|
||||
func GetArtifactsOfRun(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/artifacts repository getArtifactsOfRun
|
||||
// ---
|
||||
// summary: Lists all artifacts for a repository run
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: name of the owner
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repository
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: run
|
||||
// in: path
|
||||
// description: runid of the workflow run
|
||||
// type: integer
|
||||
// required: true
|
||||
// - name: name
|
||||
// in: query
|
||||
// description: name of the artifact
|
||||
// type: string
|
||||
// required: false
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ArtifactsList"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
repoID := ctx.Repo.Repository.ID
|
||||
artifactName := ctx.Req.URL.Query().Get("name")
|
||||
|
||||
runID := ctx.PathParamInt64("run")
|
||||
|
||||
artifacts, total, err := db.FindAndCount[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{
|
||||
RepoID: repoID,
|
||||
RunID: runID,
|
||||
ArtifactName: artifactName,
|
||||
FinalizedArtifactsV4: true,
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, err.Error(), err)
|
||||
return
|
||||
}
|
||||
|
||||
res := new(api.ActionArtifactsResponse)
|
||||
res.TotalCount = total
|
||||
|
||||
res.Entries = make([]*api.ActionArtifact, len(artifacts))
|
||||
for i := range artifacts {
|
||||
convertedArtifact, err := convert.ToActionArtifact(ctx.Repo.Repository, artifacts[i])
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ToActionArtifact", err)
|
||||
return
|
||||
}
|
||||
res.Entries[i] = convertedArtifact
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, &res)
|
||||
}
|
||||
|
||||
// GetArtifacts Lists all artifacts for a repository.
|
||||
func GetArtifacts(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/actions/artifacts repository getArtifacts
|
||||
// ---
|
||||
// summary: Lists all artifacts for a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: name of the owner
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repository
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: name
|
||||
// in: query
|
||||
// description: name of the artifact
|
||||
// type: string
|
||||
// required: false
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ArtifactsList"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
repoID := ctx.Repo.Repository.ID
|
||||
artifactName := ctx.Req.URL.Query().Get("name")
|
||||
|
||||
artifacts, total, err := db.FindAndCount[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{
|
||||
RepoID: repoID,
|
||||
ArtifactName: artifactName,
|
||||
FinalizedArtifactsV4: true,
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, err.Error(), err)
|
||||
return
|
||||
}
|
||||
|
||||
res := new(api.ActionArtifactsResponse)
|
||||
res.TotalCount = total
|
||||
|
||||
res.Entries = make([]*api.ActionArtifact, len(artifacts))
|
||||
for i := range artifacts {
|
||||
convertedArtifact, err := convert.ToActionArtifact(ctx.Repo.Repository, artifacts[i])
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ToActionArtifact", err)
|
||||
return
|
||||
}
|
||||
res.Entries[i] = convertedArtifact
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, &res)
|
||||
}
|
||||
|
||||
// GetArtifact Gets a specific artifact for a workflow run.
|
||||
func GetArtifact(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/actions/artifacts/{artifact_id} repository getArtifact
|
||||
// ---
|
||||
// summary: Gets a specific artifact for a workflow run
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: name of the owner
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repository
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: artifact_id
|
||||
// in: path
|
||||
// description: id of the artifact
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Artifact"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
art := getArtifactByPathParam(ctx, ctx.Repo.Repository)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if actions.IsArtifactV4(art) {
|
||||
convertedArtifact, err := convert.ToActionArtifact(ctx.Repo.Repository, art)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ToActionArtifact", err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convertedArtifact)
|
||||
return
|
||||
}
|
||||
// v3 not supported due to not having one unique id
|
||||
ctx.Error(http.StatusNotFound, "GetArtifact", "Artifact not found")
|
||||
}
|
||||
|
||||
// DeleteArtifact Deletes a specific artifact for a workflow run.
|
||||
func DeleteArtifact(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/actions/artifacts/{artifact_id} repository deleteArtifact
|
||||
// ---
|
||||
// summary: Deletes a specific artifact for a workflow run
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: name of the owner
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repository
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: artifact_id
|
||||
// in: path
|
||||
// description: id of the artifact
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// description: "No Content"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
art := getArtifactByPathParam(ctx, ctx.Repo.Repository)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if actions.IsArtifactV4(art) {
|
||||
if err := actions_model.SetArtifactNeedDelete(ctx, art.RunID, art.ArtifactName); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "DeleteArtifact", err)
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
// v3 not supported due to not having one unique id
|
||||
ctx.Error(http.StatusNotFound, "DeleteArtifact", "Artifact not found")
|
||||
}
|
||||
|
||||
func buildSignature(endp string, expires, artifactID int64) []byte {
|
||||
mac := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret())
|
||||
mac.Write([]byte(endp))
|
||||
mac.Write([]byte(fmt.Sprint(expires)))
|
||||
mac.Write([]byte(fmt.Sprint(artifactID)))
|
||||
return mac.Sum(nil)
|
||||
}
|
||||
|
||||
func buildDownloadRawEndpoint(repo *repo_model.Repository, artifactID int64) string {
|
||||
return fmt.Sprintf("api/v1/repos/%s/%s/actions/artifacts/%d/zip/raw", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name), artifactID)
|
||||
}
|
||||
|
||||
func buildSigURL(ctx go_context.Context, endPoint string, artifactID int64) string {
|
||||
// endPoint is a path like "api/v1/repos/owner/repo/actions/artifacts/1/zip/raw"
|
||||
expires := time.Now().Add(60 * time.Minute).Unix()
|
||||
uploadURL := httplib.GuessCurrentAppURL(ctx) + endPoint + "?sig=" + base64.URLEncoding.EncodeToString(buildSignature(endPoint, expires, artifactID)) + "&expires=" + strconv.FormatInt(expires, 10)
|
||||
return uploadURL
|
||||
}
|
||||
|
||||
// DownloadArtifact Downloads a specific artifact for a workflow run redirects to blob url.
|
||||
func DownloadArtifact(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/actions/artifacts/{artifact_id}/zip repository downloadArtifact
|
||||
// ---
|
||||
// summary: Downloads a specific artifact for a workflow run redirects to blob url
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: name of the owner
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repository
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: artifact_id
|
||||
// in: path
|
||||
// description: id of the artifact
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "302":
|
||||
// description: redirect to the blob download
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
art := getArtifactByPathParam(ctx, ctx.Repo.Repository)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
// if artifacts status is not uploaded-confirmed, treat it as not found
|
||||
if art.Status == actions_model.ArtifactStatusExpired {
|
||||
ctx.Error(http.StatusNotFound, "DownloadArtifact", "Artifact has expired")
|
||||
return
|
||||
}
|
||||
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(art.ArtifactName), art.ArtifactName))
|
||||
|
||||
if actions.IsArtifactV4(art) {
|
||||
ok, err := actions.DownloadArtifactV4ServeDirectOnly(ctx.Base, art)
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "DownloadArtifactV4ServeDirectOnly", err)
|
||||
return
|
||||
}
|
||||
|
||||
redirectURL := buildSigURL(ctx, buildDownloadRawEndpoint(ctx.Repo.Repository, art.ID), art.ID)
|
||||
ctx.Redirect(redirectURL, http.StatusFound)
|
||||
return
|
||||
}
|
||||
// v3 not supported due to not having one unique id
|
||||
ctx.Error(http.StatusNotFound, "DownloadArtifact", "Artifact not found")
|
||||
}
|
||||
|
||||
// DownloadArtifactRaw Downloads a specific artifact for a workflow run directly.
|
||||
func DownloadArtifactRaw(ctx *context.APIContext) {
|
||||
// it doesn't use repoAssignment middleware, so it needs to prepare the repo and check permission (sig) by itself
|
||||
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ctx.PathParam("username"), ctx.PathParam("reponame"))
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.NotFound()
|
||||
} else {
|
||||
ctx.InternalServerError(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
art := getArtifactByPathParam(ctx, repo)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
sigStr := ctx.Req.URL.Query().Get("sig")
|
||||
expiresStr := ctx.Req.URL.Query().Get("expires")
|
||||
sigBytes, _ := base64.URLEncoding.DecodeString(sigStr)
|
||||
expires, _ := strconv.ParseInt(expiresStr, 10, 64)
|
||||
|
||||
expectedSig := buildSignature(buildDownloadRawEndpoint(repo, art.ID), expires, art.ID)
|
||||
if !hmac.Equal(sigBytes, expectedSig) {
|
||||
ctx.Error(http.StatusUnauthorized, "DownloadArtifactRaw", "Error unauthorized")
|
||||
return
|
||||
}
|
||||
t := time.Unix(expires, 0)
|
||||
if t.Before(time.Now()) {
|
||||
ctx.Error(http.StatusUnauthorized, "DownloadArtifactRaw", "Error link expired")
|
||||
return
|
||||
}
|
||||
|
||||
// if artifacts status is not uploaded-confirmed, treat it as not found
|
||||
if art.Status == actions_model.ArtifactStatusExpired {
|
||||
ctx.Error(http.StatusNotFound, "DownloadArtifactRaw", "Artifact has expired")
|
||||
return
|
||||
}
|
||||
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(art.ArtifactName), art.ArtifactName))
|
||||
|
||||
if actions.IsArtifactV4(art) {
|
||||
err := actions.DownloadArtifactV4(ctx.Base, art)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "DownloadArtifactV4", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
// v3 not supported due to not having one unique id
|
||||
ctx.Error(http.StatusNotFound, "DownloadArtifactRaw", "artifact not found")
|
||||
}
|
||||
|
||||
// Try to get the artifact by ID and check access
|
||||
func getArtifactByPathParam(ctx *context.APIContext, repo *repo_model.Repository) *actions_model.ActionArtifact {
|
||||
artifactID := ctx.PathParamInt64("artifact_id")
|
||||
|
||||
art, ok, err := db.GetByID[actions_model.ActionArtifact](ctx, artifactID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "getArtifactByPathParam", err)
|
||||
return nil
|
||||
}
|
||||
// if artifacts status is not uploaded-confirmed, treat it as not found
|
||||
// only check RepoID here, because the repository owner may change over the time
|
||||
if !ok ||
|
||||
art.RepoID != repo.ID ||
|
||||
art.Status != actions_model.ArtifactStatusUploadConfirmed && art.Status != actions_model.ArtifactStatusExpired {
|
||||
ctx.Error(http.StatusNotFound, "getArtifactByPathParam", "artifact not found")
|
||||
return nil
|
||||
}
|
||||
return art
|
||||
}
|
||||
|
@@ -443,6 +443,20 @@ type swaggerRepoTasksList struct {
|
||||
Body api.ActionTaskResponse `json:"body"`
|
||||
}
|
||||
|
||||
// ArtifactsList
|
||||
// swagger:response ArtifactsList
|
||||
type swaggerRepoArtifactsList struct {
|
||||
// in:body
|
||||
Body api.ActionArtifactsResponse `json:"body"`
|
||||
}
|
||||
|
||||
// Artifact
|
||||
// swagger:response Artifact
|
||||
type swaggerRepoArtifact struct {
|
||||
// in:body
|
||||
Body api.ActionArtifact `json:"body"`
|
||||
}
|
||||
|
||||
// swagger:response Compare
|
||||
type swaggerCompare struct {
|
||||
// in:body
|
||||
|
@@ -26,7 +26,6 @@ import (
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
@@ -669,7 +668,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
|
||||
|
||||
// if artifacts status is not uploaded-confirmed, treat it as not found
|
||||
for _, art := range artifacts {
|
||||
if art.Status != int64(actions_model.ArtifactStatusUploadConfirmed) {
|
||||
if art.Status != actions_model.ArtifactStatusUploadConfirmed {
|
||||
ctx.Error(http.StatusNotFound, "artifact not found")
|
||||
return
|
||||
}
|
||||
@@ -677,23 +676,12 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
|
||||
|
||||
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(artifactName), artifactName))
|
||||
|
||||
// Artifacts using the v4 backend are stored as a single combined zip file per artifact on the backend
|
||||
// The v4 backend enshures ContentEncoding is set to "application/zip", which is not the case for the old backend
|
||||
if len(artifacts) == 1 && artifacts[0].ArtifactName+".zip" == artifacts[0].ArtifactPath && artifacts[0].ContentEncoding == "application/zip" {
|
||||
art := artifacts[0]
|
||||
if setting.Actions.ArtifactStorage.ServeDirect() {
|
||||
u, err := storage.ActionsArtifacts.URL(art.StoragePath, art.ArtifactPath, nil)
|
||||
if u != nil && err == nil {
|
||||
ctx.Redirect(u.String())
|
||||
return
|
||||
}
|
||||
}
|
||||
f, err := storage.ActionsArtifacts.Open(art.StoragePath)
|
||||
if len(artifacts) == 1 && actions.IsArtifactV4(artifacts[0]) {
|
||||
err := actions.DownloadArtifactV4(ctx.Base, artifacts[0])
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
_, _ = io.Copy(ctx.Resp, f)
|
||||
return
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user