mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-15 04:13:35 +02:00
fix review
This commit is contained in:
parent
49b6064074
commit
a1915f1c4d
@ -41,114 +41,22 @@ var mockActionsArtifactFiles = map[string][]mockArtifactFile{
|
||||
},
|
||||
"artifact-lcov-coverage": {
|
||||
{
|
||||
Path: "coverage/index.html",
|
||||
Content: `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Coverage Report</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 2rem; }
|
||||
table { border-collapse: collapse; width: 100%; margin-top: 1rem; }
|
||||
th, td { border: 1px solid #ddd; padding: 0.5rem; text-align: left; }
|
||||
.ok { color: #1f883d; font-weight: 600; }
|
||||
.warn { color: #9a6700; font-weight: 600; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>LCOV Coverage Report</h1>
|
||||
<p>Generated from mock fixture artifact.</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>File</th>
|
||||
<th>Lines</th>
|
||||
<th>Branches</th>
|
||||
<th>Functions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>web_src/js/components/RepoActionView.vue</td>
|
||||
<td class="ok">100.0% (7/7)</td>
|
||||
<td class="warn">75.0% (3/4)</td>
|
||||
<td class="ok">100.0% (3/3)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>web_src/js/features/repo-actions.ts</td>
|
||||
<td class="ok">100.0% (5/5)</td>
|
||||
<td>n/a</td>
|
||||
<td class="ok">100.0% (2/2)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><a href="./lcov.info">Open raw lcov.info</a></p>
|
||||
</body>
|
||||
</html>`,
|
||||
Path: "coverage/index.html",
|
||||
Content: "<html><body>mock lcov coverage report</body></html>",
|
||||
},
|
||||
{
|
||||
Path: "coverage/lcov.info",
|
||||
Content: `TN:
|
||||
SF:web_src/js/components/RepoActionView.vue
|
||||
FN:33,artifactBaseURL
|
||||
FN:37,artifactPreviewURL
|
||||
FN:41,deleteArtifact
|
||||
FNF:3
|
||||
FNH:3
|
||||
FNDA:9,artifactBaseURL
|
||||
FNDA:8,artifactPreviewURL
|
||||
FNDA:2,deleteArtifact
|
||||
DA:33,9
|
||||
DA:34,9
|
||||
DA:37,8
|
||||
DA:38,8
|
||||
DA:41,2
|
||||
DA:42,2
|
||||
DA:43,2
|
||||
LF:7
|
||||
LH:7
|
||||
BRDA:131,0,0,1
|
||||
BRDA:131,0,1,3
|
||||
BRDA:140,1,0,1
|
||||
BRDA:140,1,1,0
|
||||
BRF:4
|
||||
BRH:3
|
||||
end_of_record
|
||||
TN:
|
||||
SF:web_src/js/features/repo-actions.ts
|
||||
FN:12,loadRunView
|
||||
FN:61,fetchRunArtifacts
|
||||
FNF:2
|
||||
FNH:2
|
||||
FNDA:5,loadRunView
|
||||
FNDA:5,fetchRunArtifacts
|
||||
DA:12,5
|
||||
DA:13,5
|
||||
DA:14,5
|
||||
DA:61,5
|
||||
DA:62,5
|
||||
LF:5
|
||||
LH:5
|
||||
BRF:0
|
||||
BRH:0
|
||||
end_of_record
|
||||
`,
|
||||
Path: "coverage/lcov.info",
|
||||
Content: "TN:\nSF:mock.go\nend_of_record\n",
|
||||
},
|
||||
{
|
||||
Path: "coverage/summary.txt",
|
||||
Content: "HTML coverage fixture with linked lcov.info and realistic function/line/branch records.",
|
||||
Content: "mock coverage summary",
|
||||
},
|
||||
},
|
||||
"artifact-really-loooooooooooooooooooooooooooooooooooooooooooooooooooooooong": {
|
||||
{
|
||||
Path: "index.html",
|
||||
Content: `<!doctype html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>Mock Artifact Preview</h1>
|
||||
<p>artifact-really-loooooong</p>
|
||||
</body>
|
||||
</html>`,
|
||||
Path: "index.html",
|
||||
Content: "<html><body>mock preview</body></html>",
|
||||
},
|
||||
{
|
||||
Path: "logs/output.txt",
|
||||
|
||||
@ -4,19 +4,13 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
@ -27,15 +21,10 @@ import (
|
||||
"code.gitea.io/gitea/modules/actions"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"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/templates"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/modules/typesniffer"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/util/filebuffer"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/common"
|
||||
actions_service "code.gitea.io/gitea/services/actions"
|
||||
@ -266,55 +255,6 @@ type ViewRequest struct {
|
||||
LogCursors []LogCursor `json:"logCursors"`
|
||||
}
|
||||
|
||||
type ArtifactsViewItem struct {
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
Status string `json:"status"`
|
||||
ExpiresUnix int64 `json:"expiresUnix"`
|
||||
}
|
||||
|
||||
type ArtifactPreviewFile struct {
|
||||
Path string
|
||||
Selected bool
|
||||
}
|
||||
|
||||
const (
|
||||
artifactPreviewV4ZipListCacheTTL = 10 * time.Minute
|
||||
artifactPreviewV4ZipListCacheMaxEntries = 128
|
||||
)
|
||||
|
||||
type artifactPreviewV4ZipListCacheEntry struct {
|
||||
paths []string
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
var artifactPreviewV4ZipListCache = struct {
|
||||
mu sync.Mutex
|
||||
entries map[string]artifactPreviewV4ZipListCacheEntry
|
||||
order []string
|
||||
}{
|
||||
entries: map[string]artifactPreviewV4ZipListCacheEntry{},
|
||||
}
|
||||
|
||||
type readAtBySeeker struct {
|
||||
rs io.ReadSeeker
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (r *readAtBySeeker) ReadAt(p []byte, off int64) (int, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if _, err := r.rs.Seek(off, io.SeekStart); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n, err := io.ReadFull(r.rs, p)
|
||||
if errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
return n, io.EOF
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
type ViewResponse struct {
|
||||
Artifacts []*ArtifactsViewItem `json:"artifacts"`
|
||||
|
||||
@ -948,518 +888,6 @@ func getCurrentRunJobsByPathParam(ctx *context_module.Context) (*actions_model.A
|
||||
return run, attempt, jobs
|
||||
}
|
||||
|
||||
// resolveArtifactAttemptIDFromQuery resolves the run_attempt_id used to scope artifact lookups.
|
||||
// If the `attempt` query parameter is present and valid, it returns the matching attempt's ID.
|
||||
// Otherwise it falls back to run.LatestAttemptID, which is 0 only for legacy runs created before ActionRunAttempt existed.
|
||||
func resolveArtifactAttemptIDFromQuery(ctx *context_module.Context, run *actions_model.ActionRun) (int64, error) {
|
||||
if ctx.FormString("attempt") == "" {
|
||||
return run.LatestAttemptID, nil
|
||||
}
|
||||
attemptNum := ctx.FormInt64("attempt")
|
||||
if attemptNum <= 0 {
|
||||
return 0, util.ErrNotExist
|
||||
}
|
||||
attempt, err := actions_model.GetRunAttemptByRunIDAndAttemptNum(ctx, run.ID, attemptNum)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return attempt.ID, nil
|
||||
}
|
||||
|
||||
func getCurrentRunAndUploadedArtifacts(ctx *context_module.Context, artifactName string) (*actions_model.ActionRun, []*actions_model.ActionArtifact, bool) {
|
||||
run := getCurrentRunByPathParam(ctx)
|
||||
if ctx.Written() {
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
artifacts, err := db.Find[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{
|
||||
RunID: run.ID,
|
||||
ArtifactName: artifactName,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("FindArtifacts", err)
|
||||
return nil, nil, false
|
||||
}
|
||||
if len(artifacts) == 0 {
|
||||
ctx.HTTPError(http.StatusNotFound, "artifact not found")
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
for _, art := range artifacts {
|
||||
if art.Status != actions_model.ArtifactStatusUploadConfirmed {
|
||||
ctx.HTTPError(http.StatusNotFound, "artifact not found")
|
||||
return nil, nil, false
|
||||
}
|
||||
}
|
||||
|
||||
run.Repo = ctx.Repo.Repository
|
||||
return run, artifacts, true
|
||||
}
|
||||
|
||||
func normalizeArtifactPreviewPath(path string) string {
|
||||
path = util.PathJoinRelX(path)
|
||||
if path == "." {
|
||||
return ""
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// GetRequestedPreviewPath reads the requested artifact preview path from a
|
||||
// request, accepting either the trailing `/preview/raw/*` path segment or a
|
||||
// `?path=` query parameter, and normalizes it to a safe relative path.
|
||||
func GetRequestedPreviewPath(ctx *context_module.Context) string {
|
||||
path := strings.TrimPrefix(ctx.PathParam("*"), "/")
|
||||
if path == "" {
|
||||
path = ctx.Req.URL.Query().Get("path")
|
||||
}
|
||||
return normalizeArtifactPreviewPath(path)
|
||||
}
|
||||
|
||||
func artifactPreviewFallbackPath(artifact *actions_model.ActionArtifact) string {
|
||||
path := normalizeArtifactPreviewPath(artifact.ArtifactPath)
|
||||
if path != "" {
|
||||
return path
|
||||
}
|
||||
return artifact.ArtifactName
|
||||
}
|
||||
|
||||
// ChoosePreviewPath resolves the preview path to render.
|
||||
// An empty `requested` means no path was specified, so the first file is selected as a default.
|
||||
// A non-empty `requested` that is not present in `paths` returns "" so callers can 404 instead of silently swapping to a different file.
|
||||
func ChoosePreviewPath(paths []string, requested string) string {
|
||||
if len(paths) == 0 {
|
||||
return ""
|
||||
}
|
||||
if requested == "" {
|
||||
return paths[0]
|
||||
}
|
||||
if util.SliceContainsString(paths, requested) {
|
||||
return requested
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func listPreviewPathsForLegacyArtifacts(artifacts []*actions_model.ActionArtifact) []string {
|
||||
paths := make([]string, 0, len(artifacts))
|
||||
seen := make(map[string]struct{}, len(artifacts))
|
||||
for _, artifact := range artifacts {
|
||||
path := artifactPreviewFallbackPath(artifact)
|
||||
if _, ok := seen[path]; ok {
|
||||
continue
|
||||
}
|
||||
seen[path] = struct{}{}
|
||||
paths = append(paths, path)
|
||||
}
|
||||
sort.Strings(paths)
|
||||
return paths
|
||||
}
|
||||
|
||||
func openArtifactV4ZipReader(artifact *actions_model.ActionArtifact) (storage.Object, *zip.Reader, error) {
|
||||
f, err := storage.ActionsArtifacts.Open(artifact.StoragePath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
stat, err := f.Stat()
|
||||
if err != nil {
|
||||
_ = f.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
reader, err := zip.NewReader(&readAtBySeeker{rs: f}, stat.Size())
|
||||
if err != nil {
|
||||
_ = f.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
return f, reader, nil
|
||||
}
|
||||
|
||||
func listArtifactV4ZipFiles(reader *zip.Reader) ([]string, map[string]*zip.File) {
|
||||
paths := make([]string, 0, len(reader.File))
|
||||
files := make(map[string]*zip.File, len(reader.File))
|
||||
for _, file := range reader.File {
|
||||
if file.FileInfo().IsDir() {
|
||||
continue
|
||||
}
|
||||
path := normalizeArtifactPreviewPath(file.Name)
|
||||
if path == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := files[path]; ok {
|
||||
continue
|
||||
}
|
||||
files[path] = file
|
||||
paths = append(paths, path)
|
||||
}
|
||||
sort.Strings(paths)
|
||||
return paths, files
|
||||
}
|
||||
|
||||
func listPreviewPathsForV4Artifact(artifact *actions_model.ActionArtifact) ([]string, error) {
|
||||
if paths, ok := getArtifactPreviewV4ZipListFromCache(artifact); ok {
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
obj, reader, err := openArtifactV4ZipReader(artifact)
|
||||
if err != nil {
|
||||
if errors.Is(err, zip.ErrFormat) {
|
||||
paths := []string{artifactPreviewFallbackPath(artifact)}
|
||||
setArtifactPreviewV4ZipListCache(artifact, paths)
|
||||
return paths, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer obj.Close()
|
||||
|
||||
paths, _ := listArtifactV4ZipFiles(reader)
|
||||
setArtifactPreviewV4ZipListCache(artifact, paths)
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
func artifactPreviewV4ZipListCacheKey(artifact *actions_model.ActionArtifact) string {
|
||||
return strconv.FormatInt(artifact.ID, 10) + ":" + strconv.FormatInt(int64(artifact.UpdatedUnix), 10)
|
||||
}
|
||||
|
||||
func removeArtifactPreviewV4ZipListCacheOrderKey(order []string, key string) []string {
|
||||
for i, current := range order {
|
||||
if current != key {
|
||||
continue
|
||||
}
|
||||
return append(order[:i], order[i+1:]...)
|
||||
}
|
||||
return order
|
||||
}
|
||||
|
||||
func getArtifactPreviewV4ZipListFromCache(artifact *actions_model.ActionArtifact) ([]string, bool) {
|
||||
key := artifactPreviewV4ZipListCacheKey(artifact)
|
||||
|
||||
artifactPreviewV4ZipListCache.mu.Lock()
|
||||
entry, ok := artifactPreviewV4ZipListCache.entries[key]
|
||||
if !ok {
|
||||
artifactPreviewV4ZipListCache.mu.Unlock()
|
||||
return nil, false
|
||||
}
|
||||
if time.Now().After(entry.expiresAt) {
|
||||
delete(artifactPreviewV4ZipListCache.entries, key)
|
||||
artifactPreviewV4ZipListCache.order = removeArtifactPreviewV4ZipListCacheOrderKey(artifactPreviewV4ZipListCache.order, key)
|
||||
artifactPreviewV4ZipListCache.mu.Unlock()
|
||||
return nil, false
|
||||
}
|
||||
paths := append([]string(nil), entry.paths...)
|
||||
artifactPreviewV4ZipListCache.mu.Unlock()
|
||||
return paths, true
|
||||
}
|
||||
|
||||
func setArtifactPreviewV4ZipListCache(artifact *actions_model.ActionArtifact, paths []string) {
|
||||
key := artifactPreviewV4ZipListCacheKey(artifact)
|
||||
|
||||
artifactPreviewV4ZipListCache.mu.Lock()
|
||||
defer artifactPreviewV4ZipListCache.mu.Unlock()
|
||||
|
||||
if _, ok := artifactPreviewV4ZipListCache.entries[key]; ok {
|
||||
artifactPreviewV4ZipListCache.order = removeArtifactPreviewV4ZipListCacheOrderKey(artifactPreviewV4ZipListCache.order, key)
|
||||
}
|
||||
artifactPreviewV4ZipListCache.order = append(artifactPreviewV4ZipListCache.order, key)
|
||||
artifactPreviewV4ZipListCache.entries[key] = artifactPreviewV4ZipListCacheEntry{
|
||||
paths: append([]string(nil), paths...),
|
||||
expiresAt: time.Now().Add(artifactPreviewV4ZipListCacheTTL),
|
||||
}
|
||||
|
||||
for len(artifactPreviewV4ZipListCache.entries) > artifactPreviewV4ZipListCacheMaxEntries && len(artifactPreviewV4ZipListCache.order) > 0 {
|
||||
oldestKey := artifactPreviewV4ZipListCache.order[0]
|
||||
artifactPreviewV4ZipListCache.order = artifactPreviewV4ZipListCache.order[1:]
|
||||
if _, ok := artifactPreviewV4ZipListCache.entries[oldestKey]; !ok {
|
||||
continue
|
||||
}
|
||||
delete(artifactPreviewV4ZipListCache.entries, oldestKey)
|
||||
}
|
||||
}
|
||||
|
||||
func listPreviewPaths(artifacts []*actions_model.ActionArtifact) ([]string, error) {
|
||||
if len(artifacts) == 1 && actions.IsArtifactV4(artifacts[0]) {
|
||||
return listPreviewPathsForV4Artifact(artifacts[0])
|
||||
}
|
||||
return listPreviewPathsForLegacyArtifacts(artifacts), nil
|
||||
}
|
||||
|
||||
func isPreviewableArtifactType(st typesniffer.SniffedType) bool {
|
||||
return st.IsText() || st.IsPDF()
|
||||
}
|
||||
|
||||
func previewArtifactByReader(ctx *context_module.Context, path string, reader io.Reader) {
|
||||
buf := filebuffer.New(int(setting.UI.MaxDisplayFileSize), "")
|
||||
defer buf.Close()
|
||||
if _, err := io.Copy(buf, io.LimitReader(reader, setting.UI.MaxDisplayFileSize)); err != nil {
|
||||
ctx.ServerError("io.Copy", err)
|
||||
return
|
||||
}
|
||||
if _, err := buf.Seek(0, io.SeekStart); err != nil {
|
||||
ctx.ServerError("Seek", err)
|
||||
return
|
||||
}
|
||||
previewArtifactByReadSeeker(ctx, path, buf)
|
||||
}
|
||||
|
||||
func previewArtifactByReadSeeker(ctx *context_module.Context, path string, reader io.ReadSeeker) {
|
||||
buf := make([]byte, typesniffer.SniffContentSize)
|
||||
n, err := util.ReadAtMost(reader, buf)
|
||||
if err != nil {
|
||||
ctx.ServerError("ReadAtMost", err)
|
||||
return
|
||||
}
|
||||
buf = buf[:n]
|
||||
|
||||
if _, err := reader.Seek(0, io.SeekStart); err != nil {
|
||||
ctx.ServerError("Seek", err)
|
||||
return
|
||||
}
|
||||
|
||||
st := typesniffer.DetectContentType(buf)
|
||||
if !isPreviewableArtifactType(st) {
|
||||
ctx.HTTPError(http.StatusUnsupportedMediaType, "artifact preview is not supported for this file type")
|
||||
return
|
||||
}
|
||||
|
||||
// CSP sandbox is applied by httplib.ServeSetHeaders, see HINT: PDF-RENDER-SANDBOX
|
||||
ctx.ServeContent(reader, context_module.ServeHeaderOptions{
|
||||
Filename: path,
|
||||
ContentType: st.GetMimeType(),
|
||||
})
|
||||
}
|
||||
|
||||
func ArtifactsPreviewView(ctx *context_module.Context) {
|
||||
artifactName := ctx.PathParam("artifact_name")
|
||||
|
||||
run, artifacts, ok := getCurrentRunAndUploadedArtifacts(ctx, artifactName)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
paths, err := listPreviewPaths(artifacts)
|
||||
if err != nil {
|
||||
ctx.ServerError("listPreviewPaths", err)
|
||||
return
|
||||
}
|
||||
selectedPath := ChoosePreviewPath(paths, GetRequestedPreviewPath(ctx))
|
||||
|
||||
previewFiles := make([]ArtifactPreviewFile, 0, len(paths))
|
||||
for _, path := range paths {
|
||||
previewFiles = append(previewFiles, ArtifactPreviewFile{
|
||||
Path: path,
|
||||
Selected: path == selectedPath,
|
||||
})
|
||||
}
|
||||
|
||||
runURL := run.Link()
|
||||
artifactPath := url.PathEscape(artifactName)
|
||||
previewURL := runURL + "/artifacts/" + artifactPath + "/preview"
|
||||
|
||||
ctx.Data["Title"] = ctx.Tr("preview")
|
||||
ctx.Data["PageIsActions"] = true
|
||||
ctx.Data["RunURL"] = runURL
|
||||
ctx.Data["ArtifactName"] = artifactName
|
||||
ctx.Data["PreviewURL"] = previewURL
|
||||
ctx.Data["PreviewRawURL"] = previewURL + "/raw"
|
||||
ctx.Data["DownloadURL"] = runURL + "/artifacts/" + artifactPath
|
||||
ctx.Data["SelectedPath"] = selectedPath
|
||||
ctx.Data["PreviewFiles"] = previewFiles
|
||||
|
||||
ctx.HTML(http.StatusOK, tplArtifactPreviewAction)
|
||||
}
|
||||
|
||||
// serveArtifactV4PreviewRaw opens the v4 artifact zip once and serves a single file from it,
|
||||
// avoiding the redundant parse that listPreviewPaths would do for raw fetches.
|
||||
func serveArtifactV4PreviewRaw(ctx *context_module.Context, artifact *actions_model.ActionArtifact, requested string) {
|
||||
obj, reader, err := openArtifactV4ZipReader(artifact)
|
||||
if err != nil {
|
||||
if !errors.Is(err, zip.ErrFormat) {
|
||||
ctx.ServerError("openArtifactV4ZipReader", err)
|
||||
return
|
||||
}
|
||||
fallbackPath := artifactPreviewFallbackPath(artifact)
|
||||
selectedPath := ChoosePreviewPath([]string{fallbackPath}, requested)
|
||||
if selectedPath == "" {
|
||||
ctx.HTTPError(http.StatusNotFound, "artifact file not found")
|
||||
return
|
||||
}
|
||||
f, err := storage.ActionsArtifacts.Open(artifact.StoragePath)
|
||||
if err != nil {
|
||||
ctx.ServerError("ActionsArtifacts.Open", err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
previewArtifactByReadSeeker(ctx, selectedPath, f)
|
||||
return
|
||||
}
|
||||
defer obj.Close()
|
||||
|
||||
paths, files := listArtifactV4ZipFiles(reader)
|
||||
selectedPath := ChoosePreviewPath(paths, requested)
|
||||
if selectedPath == "" {
|
||||
ctx.HTTPError(http.StatusNotFound, "artifact file not found")
|
||||
return
|
||||
}
|
||||
zf := files[selectedPath]
|
||||
r, err := zf.Open()
|
||||
if err != nil {
|
||||
ctx.ServerError("zip.File.Open", err)
|
||||
return
|
||||
}
|
||||
defer r.Close()
|
||||
previewArtifactByReader(ctx, selectedPath, r)
|
||||
}
|
||||
|
||||
func ArtifactsPreviewRawView(ctx *context_module.Context) {
|
||||
artifactName := ctx.PathParam("artifact_name")
|
||||
|
||||
_, artifacts, ok := getCurrentRunAndUploadedArtifacts(ctx, artifactName)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
requested := GetRequestedPreviewPath(ctx)
|
||||
|
||||
if len(artifacts) == 1 && actions.IsArtifactV4(artifacts[0]) {
|
||||
serveArtifactV4PreviewRaw(ctx, artifacts[0], requested)
|
||||
return
|
||||
}
|
||||
|
||||
paths := listPreviewPathsForLegacyArtifacts(artifacts)
|
||||
selectedPath := ChoosePreviewPath(paths, requested)
|
||||
if selectedPath == "" {
|
||||
ctx.HTTPError(http.StatusNotFound, "artifact file not found")
|
||||
return
|
||||
}
|
||||
|
||||
legacyByPath := make(map[string]*actions_model.ActionArtifact, len(artifacts))
|
||||
for _, artifact := range artifacts {
|
||||
path := artifactPreviewFallbackPath(artifact)
|
||||
if _, ok := legacyByPath[path]; ok {
|
||||
continue
|
||||
}
|
||||
legacyByPath[path] = artifact
|
||||
}
|
||||
|
||||
artifact, ok := legacyByPath[selectedPath]
|
||||
if !ok {
|
||||
ctx.HTTPError(http.StatusNotFound, "artifact file not found")
|
||||
return
|
||||
}
|
||||
|
||||
f, err := storage.ActionsArtifacts.Open(artifact.StoragePath)
|
||||
if err != nil {
|
||||
ctx.ServerError("ActionsArtifacts.Open", err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if artifact.ContentEncodingOrType == actions_model.ContentEncodingV3Gzip {
|
||||
r, err := gzip.NewReader(f)
|
||||
if err != nil {
|
||||
ctx.ServerError("gzip.NewReader", err)
|
||||
return
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
previewArtifactByReader(ctx, selectedPath, r)
|
||||
return
|
||||
}
|
||||
|
||||
previewArtifactByReadSeeker(ctx, selectedPath, f)
|
||||
}
|
||||
|
||||
func ArtifactsDeleteView(ctx *context_module.Context) {
|
||||
run := getCurrentRunByPathParam(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
resolvedAttemptID, err := resolveArtifactAttemptIDFromQuery(ctx, run)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("resolveArtifactAttemptIDFromQuery", func(err error) bool {
|
||||
return errors.Is(err, util.ErrNotExist)
|
||||
}, err)
|
||||
return
|
||||
}
|
||||
artifactName := ctx.PathParam("artifact_name")
|
||||
if err := actions_model.SetArtifactNeedDeleteByRunAttempt(ctx, run.ID, resolvedAttemptID, artifactName); err != nil {
|
||||
ctx.ServerError("SetArtifactNeedDeleteByRunAttempt", err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, struct{}{})
|
||||
}
|
||||
|
||||
func ArtifactsDownloadView(ctx *context_module.Context) {
|
||||
run := getCurrentRunByPathParam(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
resolvedAttemptID, err := resolveArtifactAttemptIDFromQuery(ctx, run)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("resolveArtifactAttemptIDFromQuery", func(err error) bool {
|
||||
return errors.Is(err, util.ErrNotExist)
|
||||
}, err)
|
||||
return
|
||||
}
|
||||
artifactName := ctx.PathParam("artifact_name")
|
||||
artifacts, err := actions_model.GetArtifactsByRunAttemptAndName(ctx, run.ID, resolvedAttemptID, artifactName)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetArtifactsByRunAttemptAndName", err)
|
||||
return
|
||||
}
|
||||
if len(artifacts) == 0 {
|
||||
ctx.HTTPError(http.StatusNotFound, "artifact not found")
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Set("Content-Disposition", httplib.EncodeContentDispositionAttachment(artifactName+".zip"))
|
||||
if len(artifacts) == 1 && actions.IsArtifactV4(artifacts[0]) {
|
||||
err := actions.DownloadArtifactV4(ctx.Base, artifacts[0])
|
||||
if err != nil {
|
||||
ctx.ServerError("DownloadArtifactV4", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Artifacts using the v1-v3 backend are stored as multiple individual files per artifact on the backend
|
||||
// Those need to be zipped for download
|
||||
zipWriter := zip.NewWriter(ctx.Resp)
|
||||
defer zipWriter.Close()
|
||||
|
||||
writeArtifactToZip := func(art *actions_model.ActionArtifact) error {
|
||||
f, err := storage.ActionsArtifacts.Open(art.StoragePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ActionsArtifacts.Open: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var r io.ReadCloser = f
|
||||
if art.ContentEncodingOrType == actions_model.ContentEncodingV3Gzip {
|
||||
r, err = gzip.NewReader(f)
|
||||
if err != nil {
|
||||
return fmt.Errorf("gzip.NewReader: %w", err)
|
||||
}
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
w, err := zipWriter.Create(art.ArtifactPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("zipWriter.Create: %w", err)
|
||||
}
|
||||
_, err = io.Copy(w, r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("io.Copy: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, art := range artifacts {
|
||||
err := writeArtifactToZip(art)
|
||||
if err != nil {
|
||||
ctx.ServerError("writeArtifactToZip", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ApproveAllChecks(ctx *context_module.Context) {
|
||||
repo := ctx.Repo.Repository
|
||||
commitID := ctx.FormString("commit_id")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user