mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-11 04:55:34 +02:00
updates
This commit is contained in:
parent
00efb1af42
commit
9cd410fe85
@ -17,6 +17,7 @@ import (
|
||||
"time"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
@ -36,6 +37,106 @@ var mockActionsArtifactFiles = map[string][]mockArtifactFile{
|
||||
Content: "artifact-b report",
|
||||
},
|
||||
},
|
||||
"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/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/summary.txt",
|
||||
Content: "HTML coverage fixture with linked lcov.info and realistic function/line/branch records.",
|
||||
},
|
||||
},
|
||||
"artifact-really-loooooooooooooooooooooooooooooooooooooooooooooooooooooooong": {
|
||||
{
|
||||
Path: "index.html",
|
||||
@ -160,6 +261,11 @@ func MockActionsRunsJobs(ctx *context.Context) {
|
||||
Size: 1024 * 1024,
|
||||
Status: "completed",
|
||||
})
|
||||
resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{
|
||||
Name: "artifact-lcov-coverage",
|
||||
Size: 256 * 1024,
|
||||
Status: "completed",
|
||||
})
|
||||
resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{
|
||||
Name: "artifact-very-loooooooooooooooooooooooooooooooooooooooooooooooooooooooong",
|
||||
Size: 100 * 1024,
|
||||
@ -291,7 +397,7 @@ func MockActionsArtifactDownload(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(artifactName), artifactName))
|
||||
ctx.Resp.Header().Set("Content-Disposition", httplib.EncodeContentDispositionAttachment(artifactName+".zip"))
|
||||
writer := zip.NewWriter(ctx.Resp)
|
||||
defer writer.Close()
|
||||
for _, file := range files {
|
||||
|
||||
@ -135,6 +135,24 @@ type ArtifactPreviewFile struct {
|
||||
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
|
||||
@ -841,19 +859,84 @@ func listArtifactV4ZipFiles(reader *zip.Reader) ([]string, map[string]*zip.File)
|
||||
}
|
||||
|
||||
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) {
|
||||
return []string{artifactPreviewFallbackPath(artifact)}, nil
|
||||
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) + ":" + artifact.StoragePath
|
||||
}
|
||||
|
||||
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 = 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])
|
||||
@ -1081,7 +1164,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(artifactName), artifactName))
|
||||
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 {
|
||||
@ -1093,7 +1176,6 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
|
||||
|
||||
// Artifacts using the v1-v3 backend are stored as multiple individual files per artifact on the backend
|
||||
// Those need to be zipped for download
|
||||
ctx.Resp.Header().Set("Content-Disposition", httplib.EncodeContentDispositionAttachment(artifactName+".zip"))
|
||||
zipWriter := zip.NewWriter(ctx.Resp)
|
||||
defer zipWriter.Close()
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ package actions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
@ -45,3 +46,90 @@ func TestConvertToViewModel(t *testing.T) {
|
||||
}
|
||||
assert.Equal(t, expectedViewJobs, viewJobSteps)
|
||||
}
|
||||
|
||||
func resetArtifactPreviewV4ZipListCacheForTest() {
|
||||
artifactPreviewV4ZipListCache.mu.Lock()
|
||||
defer artifactPreviewV4ZipListCache.mu.Unlock()
|
||||
artifactPreviewV4ZipListCache.entries = map[string]artifactPreviewV4ZipListCacheEntry{}
|
||||
artifactPreviewV4ZipListCache.order = nil
|
||||
}
|
||||
|
||||
func TestArtifactPreviewV4ZipListCacheSetGet(t *testing.T) {
|
||||
resetArtifactPreviewV4ZipListCacheForTest()
|
||||
|
||||
artifact := &actions_model.ActionArtifact{
|
||||
ID: 1,
|
||||
UpdatedUnix: timeutil.TimeStamp(2),
|
||||
StoragePath: "artifact/path.zip",
|
||||
}
|
||||
paths := []string{"index.html", "logs/output.txt"}
|
||||
setArtifactPreviewV4ZipListCache(artifact, paths)
|
||||
|
||||
paths[0] = "changed"
|
||||
got, ok := getArtifactPreviewV4ZipListFromCache(artifact)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, []string{"index.html", "logs/output.txt"}, got)
|
||||
|
||||
got[0] = "changed-again"
|
||||
got2, ok := getArtifactPreviewV4ZipListFromCache(artifact)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, []string{"index.html", "logs/output.txt"}, got2)
|
||||
}
|
||||
|
||||
func TestArtifactPreviewV4ZipListCacheExpires(t *testing.T) {
|
||||
resetArtifactPreviewV4ZipListCacheForTest()
|
||||
|
||||
artifact := &actions_model.ActionArtifact{
|
||||
ID: 2,
|
||||
UpdatedUnix: timeutil.TimeStamp(3),
|
||||
StoragePath: "artifact/expired.zip",
|
||||
}
|
||||
key := artifactPreviewV4ZipListCacheKey(artifact)
|
||||
|
||||
artifactPreviewV4ZipListCache.mu.Lock()
|
||||
artifactPreviewV4ZipListCache.entries[key] = artifactPreviewV4ZipListCacheEntry{
|
||||
paths: []string{"expired.txt"},
|
||||
expiresAt: time.Now().Add(-time.Second),
|
||||
}
|
||||
artifactPreviewV4ZipListCache.order = []string{key}
|
||||
artifactPreviewV4ZipListCache.mu.Unlock()
|
||||
|
||||
_, ok := getArtifactPreviewV4ZipListFromCache(artifact)
|
||||
require.False(t, ok)
|
||||
|
||||
artifactPreviewV4ZipListCache.mu.Lock()
|
||||
_, exists := artifactPreviewV4ZipListCache.entries[key]
|
||||
order := append([]string(nil), artifactPreviewV4ZipListCache.order...)
|
||||
artifactPreviewV4ZipListCache.mu.Unlock()
|
||||
assert.False(t, exists)
|
||||
assert.NotContains(t, order, key)
|
||||
}
|
||||
|
||||
func TestArtifactPreviewV4ZipListCacheEvictsOldest(t *testing.T) {
|
||||
resetArtifactPreviewV4ZipListCacheForTest()
|
||||
|
||||
for i := range artifactPreviewV4ZipListCacheMaxEntries + 1 {
|
||||
artifact := &actions_model.ActionArtifact{
|
||||
ID: int64(i + 1),
|
||||
UpdatedUnix: timeutil.TimeStamp(i + 1),
|
||||
StoragePath: "artifact/cache-entry.zip",
|
||||
}
|
||||
setArtifactPreviewV4ZipListCache(artifact, []string{"file.txt"})
|
||||
}
|
||||
|
||||
oldest := &actions_model.ActionArtifact{
|
||||
ID: 1,
|
||||
UpdatedUnix: timeutil.TimeStamp(1),
|
||||
StoragePath: "artifact/cache-entry.zip",
|
||||
}
|
||||
_, ok := getArtifactPreviewV4ZipListFromCache(oldest)
|
||||
assert.False(t, ok)
|
||||
|
||||
newest := &actions_model.ActionArtifact{
|
||||
ID: artifactPreviewV4ZipListCacheMaxEntries + 1,
|
||||
UpdatedUnix: timeutil.TimeStamp(artifactPreviewV4ZipListCacheMaxEntries + 1),
|
||||
StoragePath: "artifact/cache-entry.zip",
|
||||
}
|
||||
_, ok = getArtifactPreviewV4ZipListFromCache(newest)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
@ -7,10 +7,6 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.artifact-preview-subtitle {
|
||||
color: var(--color-text-light-3);
|
||||
}
|
||||
|
||||
.artifact-preview-frame {
|
||||
width: 100%;
|
||||
min-height: 70vh;
|
||||
@ -22,14 +18,17 @@
|
||||
<div class="ui top attached header tw-flex tw-items-center tw-justify-between">
|
||||
<div class="artifact-preview-title">
|
||||
<span class="tw-text-base tw-font-semibold">{{ctx.Locale.Tr "preview"}}: <span class="gt-ellipsis">{{.ArtifactName}}</span></span>
|
||||
<div class="artifact-preview-subtitle tw-mt-1">
|
||||
<a href="{{.RunURL}}">{{ctx.Locale.Tr "go_back"}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<a class="ui button" href="{{.DownloadURL}}">
|
||||
{{svg "octicon-download"}}
|
||||
{{ctx.Locale.Tr "repo.download_file"}}
|
||||
</a>
|
||||
<div class="tw-flex tw-items-center tw-gap-2">
|
||||
<a class="ui basic button" href="{{.RunURL}}">
|
||||
{{svg "octicon-arrow-left"}}
|
||||
{{ctx.Locale.Tr "go_back"}}
|
||||
</a>
|
||||
<a class="ui button" href="{{.DownloadURL}}">
|
||||
{{svg "octicon-download"}}
|
||||
{{ctx.Locale.Tr "repo.download_file"}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui attached segment">
|
||||
|
||||
@ -36,6 +36,8 @@ func TestActionsArtifactPreviewSingleFile(t *testing.T) {
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.Contains(t, resp.Body.String(), "abc.txt")
|
||||
assert.Contains(t, resp.Body.String(), "/preview/raw/abc.txt")
|
||||
assert.Contains(t, resp.Body.String(), "sandbox=\"allow-scripts\"")
|
||||
assert.Contains(t, resp.Body.String(), "referrerpolicy=\"no-referrer\"")
|
||||
|
||||
req = NewRequestf(t, "GET", "/%s/actions/runs/187/artifacts/artifact-download/preview/raw", repo.FullName())
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
@ -54,7 +56,11 @@ func TestActionsArtifactPreviewMultiFile(t *testing.T) {
|
||||
assert.Contains(t, resp.Body.String(), "abc.txt")
|
||||
assert.Contains(t, resp.Body.String(), "xyz/def.txt")
|
||||
|
||||
req = NewRequestf(t, "GET", "/%s/actions/runs/187/artifacts/multi-file-download/preview/raw?path=%s", repo.FullName(), url.QueryEscape("xyz/def.txt"))
|
||||
req = NewRequestf(t, "GET", "/%s/actions/runs/187/artifacts/multi-file-download/preview?path=%s", repo.FullName(), url.QueryEscape("xyz/def.txt"))
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.Contains(t, resp.Body.String(), "/preview/raw/xyz/def.txt")
|
||||
|
||||
req = NewRequestf(t, "GET", "/%s/actions/runs/187/artifacts/multi-file-download/preview/raw/xyz/def.txt", repo.FullName())
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, strings.Repeat("C", 1024), resp.Body.String())
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user