diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 6281ff8f54..00993464e8 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -3790,6 +3790,8 @@ "actions.workflow.from_ref": "Use workflow from", "actions.workflow.has_workflow_dispatch": "This workflow has a workflow_dispatch event trigger.", "actions.workflow.has_no_workflow_dispatch": "Workflow '%s' has no workflow_dispatch event trigger.", + "actions.artifacts.preview_file_not_found": "The requested file is not present in this artifact.", + "actions.artifacts.preview_file_too_large": "This file is too large to preview. Please download the artifact to view it.", "actions.need_approval_desc": "Need approval to run workflows for fork pull request.", "actions.approve_all_success": "All workflow runs are approved successfully.", "actions.variables": "Variables", diff --git a/routers/web/devtest/mock_actions.go b/routers/web/devtest/mock_actions.go index 8057cd6170..8b35b7dcce 100644 --- a/routers/web/devtest/mock_actions.go +++ b/routers/web/devtest/mock_actions.go @@ -505,7 +505,8 @@ func MockActionsArtifactPreview(ctx *context.Context) { return } - selectedPath := actions.ChoosePreviewPath(mockArtifactFilePaths(files), actions.GetRequestedPreviewPath(ctx)) + requested := actions.GetRequestedPreviewPath(ctx) + selectedPath := actions.ChoosePreviewPath(mockArtifactFilePaths(files), requested) previewFiles := make([]actions.ArtifactPreviewFile, 0, len(files)) for _, file := range files { previewFiles = append(previewFiles, actions.ArtifactPreviewFile{ @@ -524,6 +525,9 @@ func MockActionsArtifactPreview(ctx *context.Context) { ctx.Data["PreviewRawURL"] = previewURL + "/raw" ctx.Data["DownloadURL"] = runURL + "/artifacts/" + url.PathEscape(artifactName) ctx.Data["SelectedPath"] = selectedPath + ctx.Data["RequestedPathMissing"] = requested != "" && selectedPath == "" + ctx.Data["AttemptQuery"] = "" + ctx.Data["AttemptAmpQuery"] = "" ctx.HTML(http.StatusOK, "devtest/repo-action-artifact-preview") } diff --git a/routers/web/repo/actions/view_artifact.go b/routers/web/repo/actions/view_artifact.go index 905eec8c1f..9e7c94bad2 100644 --- a/routers/web/repo/actions/view_artifact.go +++ b/routers/web/repo/actions/view_artifact.go @@ -19,7 +19,6 @@ import ( "time" actions_model "code.gitea.io/gitea/models/actions" - "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" @@ -104,12 +103,17 @@ func getCurrentRunAndUploadedArtifacts(ctx *context_module.Context, artifactName return nil, nil, false } - artifacts, err := db.Find[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{ - RunID: run.ID, - ArtifactName: artifactName, - }) + resolvedAttemptID, err := resolveArtifactAttemptIDFromQuery(ctx, run) if err != nil { - ctx.ServerError("FindArtifacts", err) + ctx.NotFoundOrServerError("resolveArtifactAttemptIDFromQuery", func(err error) bool { + return errors.Is(err, util.ErrNotExist) + }, err) + return nil, nil, false + } + + artifacts, err := actions_model.GetArtifactsByRunAttemptAndName(ctx, run.ID, resolvedAttemptID, artifactName) + if err != nil { + ctx.ServerError("GetArtifactsByRunAttemptAndName", err) return nil, nil, false } if len(artifacts) == 0 { @@ -330,13 +334,21 @@ func WritePreviewRawError(ctx *context_module.Context, status int, msg string) { } func previewArtifactByReader(ctx *context_module.Context, path string, reader io.Reader) { - buf := filebuffer.New(int(setting.UI.MaxDisplayFileSize), "") + maxSize := setting.UI.MaxDisplayFileSize + buf := filebuffer.New(int(maxSize), "") defer buf.Close() - if _, err := io.Copy(buf, io.LimitReader(reader, setting.UI.MaxDisplayFileSize)); err != nil { + // Copy maxSize+1 bytes so we can detect truncation: if the reader still has + // data after the limit, the file is too large to render in the preview. + n, err := io.Copy(buf, io.LimitReader(reader, maxSize+1)) + if err != nil { log.Error("artifact preview io.Copy: %v", err) WritePreviewRawError(ctx, http.StatusInternalServerError, "failed to read artifact") return } + if n > maxSize { + WritePreviewRawError(ctx, http.StatusRequestEntityTooLarge, "file is too large to preview, please download the artifact instead") + return + } if _, err := buf.Seek(0, io.SeekStart); err != nil { log.Error("artifact preview Seek: %v", err) WritePreviewRawError(ctx, http.StatusInternalServerError, "failed to read artifact") @@ -387,7 +399,8 @@ func ArtifactsPreviewView(ctx *context_module.Context) { ctx.ServerError("listPreviewPaths", err) return } - selectedPath := ChoosePreviewPath(paths, GetRequestedPreviewPath(ctx)) + requested := GetRequestedPreviewPath(ctx) + selectedPath := ChoosePreviewPath(paths, requested) previewFiles := make([]ArtifactPreviewFile, 0, len(paths)) for _, path := range paths { @@ -400,6 +413,12 @@ func ArtifactsPreviewView(ctx *context_module.Context) { runURL := run.Link() artifactPath := url.PathEscape(artifactName) previewURL := runURL + "/artifacts/" + artifactPath + "/preview" + downloadURL := runURL + "/artifacts/" + artifactPath + attemptQuery, attemptAmpQuery := "", "" + if attempt := ctx.FormString("attempt"); attempt != "" { + attemptQuery = "?attempt=" + url.QueryEscape(attempt) + attemptAmpQuery = "&attempt=" + url.QueryEscape(attempt) + } ctx.Data["Title"] = ctx.Tr("preview") ctx.Data["PageIsActions"] = true @@ -407,8 +426,11 @@ func ArtifactsPreviewView(ctx *context_module.Context) { ctx.Data["ArtifactName"] = artifactName ctx.Data["PreviewURL"] = previewURL ctx.Data["PreviewRawURL"] = previewURL + "/raw" - ctx.Data["DownloadURL"] = runURL + "/artifacts/" + artifactPath + ctx.Data["DownloadURL"] = downloadURL + attemptQuery + ctx.Data["AttemptQuery"] = attemptQuery + ctx.Data["AttemptAmpQuery"] = attemptAmpQuery ctx.Data["SelectedPath"] = selectedPath + ctx.Data["RequestedPathMissing"] = requested != "" && selectedPath == "" ctx.Data["PreviewFiles"] = previewFiles ctx.HTML(http.StatusOK, tplArtifactPreviewAction) diff --git a/templates/shared/actions/artifact_preview_content.tmpl b/templates/shared/actions/artifact_preview_content.tmpl index 6d33a75ab5..3588d791cb 100644 --- a/templates/shared/actions/artifact_preview_content.tmpl +++ b/templates/shared/actions/artifact_preview_content.tmpl @@ -1,20 +1,3 @@ - -
{{ctx.Locale.Tr "preview"}}: {{.ArtifactName}} @@ -39,7 +22,7 @@ {{ctx.Locale.Tr "files"}}
{{range .PreviewFiles}} - + {{.Path}} {{else}} @@ -51,7 +34,9 @@ {{if .SelectedPath}} {{/* sandbox="allow-scripts": scripts execute but in a null/opaque origin (no allow-same-origin), so they cannot access Gitea cookies or the parent frame. The response CSP reinforces this and blocks connect-src. */}} - + + {{else if .RequestedPathMissing}} +
{{ctx.Locale.Tr "actions.artifacts.preview_file_not_found"}}
{{else}}
{{ctx.Locale.Tr "none"}}
{{end}} diff --git a/web_src/css/features/actions-artifact-preview.css b/web_src/css/features/actions-artifact-preview.css new file mode 100644 index 0000000000..e793247fa1 --- /dev/null +++ b/web_src/css/features/actions-artifact-preview.css @@ -0,0 +1,14 @@ +.artifact-preview-page { + margin-top: 1rem; +} + +.artifact-preview-title { + min-width: 0; +} + +.artifact-preview-frame { + width: 100%; + min-height: 70vh; + border: 1px solid var(--color-secondary); + border-radius: var(--border-radius); +} diff --git a/web_src/css/index.css b/web_src/css/index.css index d7f57e324b..6c4a70f7e1 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -48,6 +48,7 @@ @import "./features/cropper.css"; @import "./features/console.css"; @import "./features/captcha.css"; +@import "./features/actions-artifact-preview.css"; @import "./markup/content.css"; @import "./markup/codeblock.css";