mirror of https://github.com/go-gitea/gitea.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
581 lines
19 KiB
581 lines
19 KiB
// Copyright 2024 The Gitea Authors. All rights reserved. |
|
// SPDX-License-Identifier: MIT |
|
|
|
package actions |
|
|
|
// GitHub Actions Artifacts V4 API Simple Description |
|
// |
|
// 1. Upload artifact |
|
// 1.1. CreateArtifact |
|
// Post: /twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact |
|
// Request: |
|
// { |
|
// "workflow_run_backend_id": "21", |
|
// "workflow_job_run_backend_id": "49", |
|
// "name": "test", |
|
// "version": 4 |
|
// } |
|
// Response: |
|
// { |
|
// "ok": true, |
|
// "signedUploadUrl": "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" |
|
// } |
|
// 1.2. Upload Zip Content to Blobstorage (unauthenticated request) |
|
// 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=block |
|
// 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 |
|
// 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"?> |
|
// <BlockList> |
|
// <Latest>blockId1</Latest> |
|
// <Latest>blockId2</Latest> |
|
// </BlockList> |
|
// 1.5. FinalizeArtifact |
|
// Post: /twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact |
|
// Request |
|
// { |
|
// "workflow_run_backend_id": "21", |
|
// "workflow_job_run_backend_id": "49", |
|
// "name": "test", |
|
// "size": "2097", |
|
// "hash": "sha256:b6325614d5649338b87215d9536b3c0477729b8638994c74cdefacb020a2cad4" |
|
// } |
|
// Response |
|
// { |
|
// "ok": true, |
|
// "artifactId": "4" |
|
// } |
|
// 2. Download artifact |
|
// 2.1. ListArtifacts and optionally filter by artifact exact name or id |
|
// Post: /twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts |
|
// Request |
|
// { |
|
// "workflow_run_backend_id": "21", |
|
// "workflow_job_run_backend_id": "49", |
|
// "name_filter": "test" |
|
// } |
|
// Response |
|
// { |
|
// "artifacts": [ |
|
// { |
|
// "workflowRunBackendId": "21", |
|
// "workflowJobRunBackendId": "49", |
|
// "databaseId": "4", |
|
// "name": "test", |
|
// "size": "2093", |
|
// "createdAt": "2024-01-23T00:13:28Z" |
|
// } |
|
// ] |
|
// } |
|
// 2.2. GetSignedArtifactURL get the URL to download the artifact zip file of a specific artifact |
|
// Post: /twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL |
|
// Request |
|
// { |
|
// "workflow_run_backend_id": "21", |
|
// "workflow_job_run_backend_id": "49", |
|
// "name": "test" |
|
// } |
|
// Response |
|
// { |
|
// "signedUrl": "http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/DownloadArtifact?sig=wHzFOwpF-6220-5CA0CIRmAX9VbiTC2Mji89UOqo1E8=&expires=2024-01-23+21%3A51%3A56.872846295+%2B0100+CET&artifactName=test&taskID=76" |
|
// } |
|
// 2.3. Download Zip from Blobstorage (unauthenticated request) |
|
// GET: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/DownloadArtifact?sig=wHzFOwpF-6220-5CA0CIRmAX9VbiTC2Mji89UOqo1E8=&expires=2024-01-23+21%3A51%3A56.872846295+%2B0100+CET&artifactName=test&taskID=76 |
|
|
|
import ( |
|
"crypto/hmac" |
|
"crypto/sha256" |
|
"encoding/base64" |
|
"encoding/xml" |
|
"fmt" |
|
"io" |
|
"net/http" |
|
"net/url" |
|
"strconv" |
|
"strings" |
|
"time" |
|
|
|
"code.gitea.io/gitea/models/actions" |
|
"code.gitea.io/gitea/models/db" |
|
"code.gitea.io/gitea/modules/httplib" |
|
"code.gitea.io/gitea/modules/log" |
|
"code.gitea.io/gitea/modules/setting" |
|
"code.gitea.io/gitea/modules/storage" |
|
"code.gitea.io/gitea/modules/util" |
|
"code.gitea.io/gitea/modules/web" |
|
"code.gitea.io/gitea/services/context" |
|
|
|
"google.golang.org/protobuf/encoding/protojson" |
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect" |
|
"google.golang.org/protobuf/types/known/timestamppb" |
|
) |
|
|
|
const ( |
|
ArtifactV4RouteBase = "/twirp/github.actions.results.api.v1.ArtifactService" |
|
ArtifactV4ContentEncoding = "application/zip" |
|
) |
|
|
|
type artifactV4Routes struct { |
|
prefix string |
|
fs storage.ObjectStorage |
|
} |
|
|
|
func ArtifactV4Contexter() func(next http.Handler) http.Handler { |
|
return func(next http.Handler) http.Handler { |
|
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { |
|
base, baseCleanUp := context.NewBaseContext(resp, req) |
|
defer baseCleanUp() |
|
|
|
ctx := &ArtifactContext{Base: base} |
|
ctx.AppendContextValue(artifactContextKey, ctx) |
|
|
|
next.ServeHTTP(ctx.Resp, ctx.Req) |
|
}) |
|
} |
|
} |
|
|
|
func ArtifactsV4Routes(prefix string) *web.Router { |
|
m := web.NewRouter() |
|
|
|
r := artifactV4Routes{ |
|
prefix: prefix, |
|
fs: storage.ActionsArtifacts, |
|
} |
|
|
|
m.Group("", func() { |
|
m.Post("CreateArtifact", r.createArtifact) |
|
m.Post("FinalizeArtifact", r.finalizeArtifact) |
|
m.Post("ListArtifacts", r.listArtifacts) |
|
m.Post("GetSignedArtifactURL", r.getSignedArtifactURL) |
|
m.Post("DeleteArtifact", r.deleteArtifact) |
|
}, ArtifactContexter()) |
|
m.Group("", func() { |
|
m.Put("UploadArtifact", r.uploadArtifact) |
|
m.Get("DownloadArtifact", r.downloadArtifact) |
|
}, ArtifactV4Contexter()) |
|
|
|
return m |
|
} |
|
|
|
func (r artifactV4Routes) buildSignature(endp, expires, artifactName string, taskID, artifactID int64) []byte { |
|
mac := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret()) |
|
mac.Write([]byte(endp)) |
|
mac.Write([]byte(expires)) |
|
mac.Write([]byte(artifactName)) |
|
mac.Write([]byte(fmt.Sprint(taskID))) |
|
mac.Write([]byte(fmt.Sprint(artifactID))) |
|
return mac.Sum(nil) |
|
} |
|
|
|
func (r artifactV4Routes) buildArtifactURL(ctx *ArtifactContext, endp, artifactName string, taskID, artifactID int64) string { |
|
expires := time.Now().Add(60 * time.Minute).Format("2006-01-02 15:04:05.999999999 -0700 MST") |
|
uploadURL := strings.TrimSuffix(httplib.GuessCurrentAppURL(ctx), "/") + strings.TrimSuffix(r.prefix, "/") + |
|
"/" + endp + "?sig=" + base64.URLEncoding.EncodeToString(r.buildSignature(endp, expires, artifactName, taskID, artifactID)) + "&expires=" + url.QueryEscape(expires) + "&artifactName=" + url.QueryEscape(artifactName) + "&taskID=" + fmt.Sprint(taskID) + "&artifactID=" + fmt.Sprint(artifactID) |
|
return uploadURL |
|
} |
|
|
|
func (r artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (*actions.ActionTask, string, bool) { |
|
rawTaskID := ctx.Req.URL.Query().Get("taskID") |
|
rawArtifactID := ctx.Req.URL.Query().Get("artifactID") |
|
sig := ctx.Req.URL.Query().Get("sig") |
|
expires := ctx.Req.URL.Query().Get("expires") |
|
artifactName := ctx.Req.URL.Query().Get("artifactName") |
|
dsig, _ := base64.URLEncoding.DecodeString(sig) |
|
taskID, _ := strconv.ParseInt(rawTaskID, 10, 64) |
|
artifactID, _ := strconv.ParseInt(rawArtifactID, 10, 64) |
|
|
|
expecedsig := r.buildSignature(endp, expires, artifactName, taskID, artifactID) |
|
if !hmac.Equal(dsig, expecedsig) { |
|
log.Error("Error unauthorized") |
|
ctx.Error(http.StatusUnauthorized, "Error unauthorized") |
|
return nil, "", false |
|
} |
|
t, err := time.Parse("2006-01-02 15:04:05.999999999 -0700 MST", expires) |
|
if err != nil || t.Before(time.Now()) { |
|
log.Error("Error link expired") |
|
ctx.Error(http.StatusUnauthorized, "Error link expired") |
|
return nil, "", false |
|
} |
|
task, err := actions.GetTaskByID(ctx, taskID) |
|
if err != nil { |
|
log.Error("Error runner api getting task by ID: %v", err) |
|
ctx.Error(http.StatusInternalServerError, "Error runner api getting task by ID") |
|
return nil, "", false |
|
} |
|
if task.Status != actions.StatusRunning { |
|
log.Error("Error runner api getting task: task is not running") |
|
ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running") |
|
return nil, "", false |
|
} |
|
if err := task.LoadJob(ctx); err != nil { |
|
log.Error("Error runner api getting job: %v", err) |
|
ctx.Error(http.StatusInternalServerError, "Error runner api getting job") |
|
return nil, "", false |
|
} |
|
return task, artifactName, true |
|
} |
|
|
|
func (r *artifactV4Routes) getArtifactByName(ctx *ArtifactContext, runID int64, name string) (*actions.ActionArtifact, error) { |
|
var art actions.ActionArtifact |
|
has, err := db.GetEngine(ctx).Where("run_id = ? AND artifact_name = ? AND artifact_path = ? AND content_encoding = ?", runID, name, name+".zip", ArtifactV4ContentEncoding).Get(&art) |
|
if err != nil { |
|
return nil, err |
|
} else if !has { |
|
return nil, util.ErrNotExist |
|
} |
|
return &art, nil |
|
} |
|
|
|
func (r *artifactV4Routes) parseProtbufBody(ctx *ArtifactContext, req protoreflect.ProtoMessage) bool { |
|
body, err := io.ReadAll(ctx.Req.Body) |
|
if err != nil { |
|
log.Error("Error decode request body: %v", err) |
|
ctx.Error(http.StatusInternalServerError, "Error decode request body") |
|
return false |
|
} |
|
err = protojson.Unmarshal(body, req) |
|
if err != nil { |
|
log.Error("Error decode request body: %v", err) |
|
ctx.Error(http.StatusInternalServerError, "Error decode request body") |
|
return false |
|
} |
|
return true |
|
} |
|
|
|
func (r *artifactV4Routes) sendProtbufBody(ctx *ArtifactContext, req protoreflect.ProtoMessage) { |
|
resp, err := protojson.Marshal(req) |
|
if err != nil { |
|
log.Error("Error encode response body: %v", err) |
|
ctx.Error(http.StatusInternalServerError, "Error encode response body") |
|
return |
|
} |
|
ctx.Resp.Header().Set("Content-Type", "application/json;charset=utf-8") |
|
ctx.Resp.WriteHeader(http.StatusOK) |
|
_, _ = ctx.Resp.Write(resp) |
|
} |
|
|
|
func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) { |
|
var req CreateArtifactRequest |
|
|
|
if ok := r.parseProtbufBody(ctx, &req); !ok { |
|
return |
|
} |
|
_, _, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId) |
|
if !ok { |
|
return |
|
} |
|
|
|
artifactName := req.Name |
|
|
|
rententionDays := setting.Actions.ArtifactRetentionDays |
|
if req.ExpiresAt != nil { |
|
rententionDays = int64(time.Until(req.ExpiresAt.AsTime()).Hours() / 24) |
|
} |
|
// create or get artifact with name and path |
|
artifact, err := actions.CreateArtifact(ctx, ctx.ActionTask, artifactName, artifactName+".zip", rententionDays) |
|
if err != nil { |
|
log.Error("Error create or get artifact: %v", err) |
|
ctx.Error(http.StatusInternalServerError, "Error create or get artifact") |
|
return |
|
} |
|
artifact.ContentEncoding = ArtifactV4ContentEncoding |
|
artifact.FileSize = 0 |
|
artifact.FileCompressedSize = 0 |
|
if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { |
|
log.Error("Error UpdateArtifactByID: %v", err) |
|
ctx.Error(http.StatusInternalServerError, "Error UpdateArtifactByID") |
|
return |
|
} |
|
|
|
respData := CreateArtifactResponse{ |
|
Ok: true, |
|
SignedUploadUrl: r.buildArtifactURL(ctx, "UploadArtifact", artifactName, ctx.ActionTask.ID, artifact.ID), |
|
} |
|
r.sendProtbufBody(ctx, &respData) |
|
} |
|
|
|
func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) { |
|
task, artifactName, ok := r.verifySignature(ctx, "UploadArtifact") |
|
if !ok { |
|
return |
|
} |
|
|
|
comp := ctx.Req.URL.Query().Get("comp") |
|
switch comp { |
|
case "block", "appendBlock": |
|
blockid := ctx.Req.URL.Query().Get("blockid") |
|
if blockid == "" { |
|
// get artifact by name |
|
artifact, err := r.getArtifactByName(ctx, task.Job.RunID, artifactName) |
|
if err != nil { |
|
log.Error("Error artifact not found: %v", err) |
|
ctx.Error(http.StatusNotFound, "Error artifact not found") |
|
return |
|
} |
|
|
|
_, err = appendUploadChunk(r.fs, ctx, artifact, artifact.FileSize, ctx.Req.ContentLength, artifact.RunID) |
|
if err != nil { |
|
log.Error("Error runner api getting task: task is not running") |
|
ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running") |
|
return |
|
} |
|
artifact.FileCompressedSize += ctx.Req.ContentLength |
|
artifact.FileSize += ctx.Req.ContentLength |
|
if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { |
|
log.Error("Error UpdateArtifactByID: %v", err) |
|
ctx.Error(http.StatusInternalServerError, "Error UpdateArtifactByID") |
|
return |
|
} |
|
} else { |
|
_, err := r.fs.Save(fmt.Sprintf("tmpv4%d/block-%d-%d-%s", task.Job.RunID, task.Job.RunID, ctx.Req.ContentLength, base64.URLEncoding.EncodeToString([]byte(blockid))), ctx.Req.Body, -1) |
|
if err != nil { |
|
log.Error("Error runner api getting task: task is not running") |
|
ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running") |
|
return |
|
} |
|
} |
|
ctx.JSON(http.StatusCreated, "appended") |
|
case "blocklist": |
|
rawArtifactID := ctx.Req.URL.Query().Get("artifactID") |
|
artifactID, _ := strconv.ParseInt(rawArtifactID, 10, 64) |
|
_, err := r.fs.Save(fmt.Sprintf("tmpv4%d/%d-%d-blocklist", task.Job.RunID, task.Job.RunID, artifactID), ctx.Req.Body, -1) |
|
if err != nil { |
|
log.Error("Error runner api getting task: task is not running") |
|
ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running") |
|
return |
|
} |
|
ctx.JSON(http.StatusCreated, "created") |
|
} |
|
} |
|
|
|
type BlockList struct { |
|
Latest []string `xml:"Latest"` |
|
} |
|
|
|
type Latest struct { |
|
Value string `xml:",chardata"` |
|
} |
|
|
|
func (r *artifactV4Routes) readBlockList(runID, artifactID int64) (*BlockList, error) { |
|
blockListName := fmt.Sprintf("tmpv4%d/%d-%d-blocklist", runID, runID, artifactID) |
|
s, err := r.fs.Open(blockListName) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
xdec := xml.NewDecoder(s) |
|
blockList := &BlockList{} |
|
err = xdec.Decode(blockList) |
|
|
|
delerr := r.fs.Delete(blockListName) |
|
if delerr != nil { |
|
log.Warn("Failed to delete blockList %s: %v", blockListName, delerr) |
|
} |
|
return blockList, err |
|
} |
|
|
|
func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) { |
|
var req FinalizeArtifactRequest |
|
|
|
if ok := r.parseProtbufBody(ctx, &req); !ok { |
|
return |
|
} |
|
_, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId) |
|
if !ok { |
|
return |
|
} |
|
|
|
// get artifact by name |
|
artifact, err := r.getArtifactByName(ctx, runID, req.Name) |
|
if err != nil { |
|
log.Error("Error artifact not found: %v", err) |
|
ctx.Error(http.StatusNotFound, "Error artifact not found") |
|
return |
|
} |
|
|
|
var chunks []*chunkFileItem |
|
blockList, err := r.readBlockList(runID, artifact.ID) |
|
if err != nil { |
|
log.Warn("Failed to read BlockList, fallback to old behavior: %v", err) |
|
chunkMap, err := listChunksByRunID(r.fs, runID) |
|
if err != nil { |
|
log.Error("Error merge chunks: %v", err) |
|
ctx.Error(http.StatusInternalServerError, "Error merge chunks") |
|
return |
|
} |
|
chunks, ok = chunkMap[artifact.ID] |
|
if !ok { |
|
log.Error("Error merge chunks") |
|
ctx.Error(http.StatusInternalServerError, "Error merge chunks") |
|
return |
|
} |
|
} else { |
|
chunks, err = listChunksByRunIDV4(r.fs, runID, artifact.ID, blockList) |
|
if err != nil { |
|
log.Error("Error merge chunks: %v", err) |
|
ctx.Error(http.StatusInternalServerError, "Error merge chunks") |
|
return |
|
} |
|
artifact.FileSize = chunks[len(chunks)-1].End + 1 |
|
artifact.FileCompressedSize = chunks[len(chunks)-1].End + 1 |
|
} |
|
|
|
checksum := "" |
|
if req.Hash != nil { |
|
checksum = req.Hash.Value |
|
} |
|
if err := mergeChunksForArtifact(ctx, chunks, r.fs, artifact, checksum); err != nil { |
|
log.Error("Error merge chunks: %v", err) |
|
ctx.Error(http.StatusInternalServerError, "Error merge chunks") |
|
return |
|
} |
|
|
|
respData := FinalizeArtifactResponse{ |
|
Ok: true, |
|
ArtifactId: artifact.ID, |
|
} |
|
r.sendProtbufBody(ctx, &respData) |
|
} |
|
|
|
func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) { |
|
var req ListArtifactsRequest |
|
|
|
if ok := r.parseProtbufBody(ctx, &req); !ok { |
|
return |
|
} |
|
_, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId) |
|
if !ok { |
|
return |
|
} |
|
|
|
artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{RunID: runID}) |
|
if err != nil { |
|
log.Error("Error getting artifacts: %v", err) |
|
ctx.Error(http.StatusInternalServerError, err.Error()) |
|
return |
|
} |
|
if len(artifacts) == 0 { |
|
log.Debug("[artifact] handleListArtifacts, no artifacts") |
|
ctx.Error(http.StatusNotFound) |
|
return |
|
} |
|
|
|
list := []*ListArtifactsResponse_MonolithArtifact{} |
|
|
|
table := map[string]*ListArtifactsResponse_MonolithArtifact{} |
|
for _, artifact := range artifacts { |
|
if _, ok := table[artifact.ArtifactName]; ok || req.IdFilter != nil && artifact.ID != req.IdFilter.Value || req.NameFilter != nil && artifact.ArtifactName != req.NameFilter.Value || artifact.ArtifactName+".zip" != artifact.ArtifactPath || artifact.ContentEncoding != ArtifactV4ContentEncoding { |
|
table[artifact.ArtifactName] = nil |
|
continue |
|
} |
|
|
|
table[artifact.ArtifactName] = &ListArtifactsResponse_MonolithArtifact{ |
|
Name: artifact.ArtifactName, |
|
CreatedAt: timestamppb.New(artifact.CreatedUnix.AsTime()), |
|
DatabaseId: artifact.ID, |
|
WorkflowRunBackendId: req.WorkflowRunBackendId, |
|
WorkflowJobRunBackendId: req.WorkflowJobRunBackendId, |
|
Size: artifact.FileSize, |
|
} |
|
} |
|
for _, artifact := range table { |
|
if artifact != nil { |
|
list = append(list, artifact) |
|
} |
|
} |
|
|
|
respData := ListArtifactsResponse{ |
|
Artifacts: list, |
|
} |
|
r.sendProtbufBody(ctx, &respData) |
|
} |
|
|
|
func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) { |
|
var req GetSignedArtifactURLRequest |
|
|
|
if ok := r.parseProtbufBody(ctx, &req); !ok { |
|
return |
|
} |
|
_, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId) |
|
if !ok { |
|
return |
|
} |
|
|
|
artifactName := req.Name |
|
|
|
// get artifact by name |
|
artifact, err := r.getArtifactByName(ctx, runID, artifactName) |
|
if err != nil { |
|
log.Error("Error artifact not found: %v", err) |
|
ctx.Error(http.StatusNotFound, "Error artifact not found") |
|
return |
|
} |
|
|
|
respData := GetSignedArtifactURLResponse{} |
|
|
|
if setting.Actions.ArtifactStorage.ServeDirect() { |
|
u, err := storage.ActionsArtifacts.URL(artifact.StoragePath, artifact.ArtifactPath, nil) |
|
if u != nil && err == nil { |
|
respData.SignedUrl = u.String() |
|
} |
|
} |
|
if respData.SignedUrl == "" { |
|
respData.SignedUrl = r.buildArtifactURL(ctx, "DownloadArtifact", artifactName, ctx.ActionTask.ID, artifact.ID) |
|
} |
|
r.sendProtbufBody(ctx, &respData) |
|
} |
|
|
|
func (r *artifactV4Routes) downloadArtifact(ctx *ArtifactContext) { |
|
task, artifactName, ok := r.verifySignature(ctx, "DownloadArtifact") |
|
if !ok { |
|
return |
|
} |
|
|
|
// get artifact by name |
|
artifact, err := r.getArtifactByName(ctx, task.Job.RunID, artifactName) |
|
if err != nil { |
|
log.Error("Error artifact not found: %v", err) |
|
ctx.Error(http.StatusNotFound, "Error artifact not found") |
|
return |
|
} |
|
|
|
file, _ := r.fs.Open(artifact.StoragePath) |
|
|
|
_, _ = io.Copy(ctx.Resp, file) |
|
} |
|
|
|
func (r *artifactV4Routes) deleteArtifact(ctx *ArtifactContext) { |
|
var req DeleteArtifactRequest |
|
|
|
if ok := r.parseProtbufBody(ctx, &req); !ok { |
|
return |
|
} |
|
_, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId) |
|
if !ok { |
|
return |
|
} |
|
|
|
// get artifact by name |
|
artifact, err := r.getArtifactByName(ctx, runID, req.Name) |
|
if err != nil { |
|
log.Error("Error artifact not found: %v", err) |
|
ctx.Error(http.StatusNotFound, "Error artifact not found") |
|
return |
|
} |
|
|
|
err = actions.SetArtifactNeedDelete(ctx, runID, req.Name) |
|
if err != nil { |
|
log.Error("Error deleting artifacts: %v", err) |
|
ctx.Error(http.StatusInternalServerError, err.Error()) |
|
return |
|
} |
|
|
|
respData := DeleteArtifactResponse{ |
|
Ok: true, |
|
ArtifactId: artifact.ID, |
|
} |
|
r.sendProtbufBody(ctx, &respData) |
|
}
|
|
|