Hi! This pull request adds support for downloading raw task logs for Gitea Actions, similar to Github Actions It looks like the following: ![image](https://user-images.githubusercontent.com/945339/235376746-405d5019-710b-468b-8113-9e82eab8e752.png)tags/v1.21.0-rc0
@@ -73,7 +73,7 @@ func WriteLogs(ctx context.Context, filename string, offset int64, rows []*runne | |||
} | |||
func ReadLogs(ctx context.Context, inStorage bool, filename string, offset, limit int64) ([]*runnerv1.LogRow, error) { | |||
f, err := openLogs(ctx, inStorage, filename) | |||
f, err := OpenLogs(ctx, inStorage, filename) | |||
if err != nil { | |||
return nil, err | |||
} | |||
@@ -141,7 +141,7 @@ func RemoveLogs(ctx context.Context, inStorage bool, filename string) error { | |||
return nil | |||
} | |||
func openLogs(ctx context.Context, inStorage bool, filename string) (io.ReadSeekCloser, error) { | |||
func OpenLogs(ctx context.Context, inStorage bool, filename string) (io.ReadSeekCloser, error) { | |||
if !inStorage { | |||
name := DBFSPrefix + filename | |||
f, err := dbfs.Open(ctx, name) |
@@ -129,6 +129,7 @@ concept_user_organization = Organization | |||
show_timestamps = Show timestamps | |||
show_log_seconds = Show seconds | |||
show_full_screen = Show full screen | |||
download_logs = Download logs | |||
confirm_delete_selected = Confirm to delete all selected items? | |||
@@ -8,6 +8,7 @@ import ( | |||
"errors" | |||
"fmt" | |||
"net/http" | |||
"strings" | |||
"time" | |||
actions_model "code.gitea.io/gitea/models/actions" | |||
@@ -310,6 +311,55 @@ func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob) erro | |||
return nil | |||
} | |||
func Logs(ctx *context_module.Context) { | |||
runIndex := ctx.ParamsInt64("run") | |||
jobIndex := ctx.ParamsInt64("job") | |||
job, _ := getRunJobs(ctx, runIndex, jobIndex) | |||
if ctx.Written() { | |||
return | |||
} | |||
if job.TaskID == 0 { | |||
ctx.Error(http.StatusNotFound, "job is not started") | |||
return | |||
} | |||
err := job.LoadRun(ctx) | |||
if err != nil { | |||
ctx.Error(http.StatusInternalServerError, err.Error()) | |||
return | |||
} | |||
task, err := actions_model.GetTaskByID(ctx, job.TaskID) | |||
if err != nil { | |||
ctx.Error(http.StatusInternalServerError, err.Error()) | |||
return | |||
} | |||
if task.LogExpired { | |||
ctx.Error(http.StatusNotFound, "logs have been cleaned up") | |||
return | |||
} | |||
reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename) | |||
if err != nil { | |||
ctx.Error(http.StatusInternalServerError, err.Error()) | |||
return | |||
} | |||
defer reader.Close() | |||
workflowName := job.Run.WorkflowID | |||
if p := strings.Index(workflowName, "."); p > 0 { | |||
workflowName = workflowName[0:p] | |||
} | |||
ctx.ServeContent(reader, &context_module.ServeHeaderOptions{ | |||
Filename: fmt.Sprintf("%v-%v-%v.log", workflowName, job.Name, task.ID), | |||
ContentLength: &task.LogSize, | |||
ContentType: "text/plain", | |||
ContentTypeCharset: "utf-8", | |||
Disposition: "attachment", | |||
}) | |||
} | |||
func Cancel(ctx *context_module.Context) { | |||
runIndex := ctx.ParamsInt64("run") | |||
@@ -1207,6 +1207,7 @@ func registerRoutes(m *web.Route) { | |||
Get(actions.View). | |||
Post(web.Bind(actions.ViewRequest{}), actions.ViewPost) | |||
m.Post("/rerun", reqRepoActionsWriter, actions.RerunOne) | |||
m.Get("/logs", actions.Logs) | |||
}) | |||
m.Post("/cancel", reqRepoActionsWriter, actions.Cancel) | |||
m.Post("/approve", reqRepoActionsWriter, actions.Approve) |
@@ -22,6 +22,7 @@ | |||
data-locale-show-timestamps="{{.locale.Tr "show_timestamps"}}" | |||
data-locale-show-log-seconds="{{.locale.Tr "show_log_seconds"}}" | |||
data-locale-show-full-screen="{{.locale.Tr "show_full_screen"}}" | |||
data-locale-download-logs="{{.locale.Tr "download_logs"}}" | |||
> | |||
</div> | |||
</div> |
@@ -74,6 +74,10 @@ | |||
<SvgIcon name="octicon-gear" :size="18"/> | |||
</button> | |||
<div class="menu transition action-job-menu" :class="{visible: menuVisible}" v-if="menuVisible" v-cloak> | |||
<a class="item" :href="run.link+'/jobs/'+jobIndex+'/logs'" target="_blank"> | |||
<i class="icon"><SvgIcon name="octicon-download"/></i> | |||
{{ locale.downloadLogs }} | |||
</a> | |||
<a class="item" @click="toggleTimeDisplay('seconds')"> | |||
<i class="icon"><SvgIcon v-show="timeVisible['log-time-seconds']" name="octicon-check"/></i> | |||
{{ locale.showLogSeconds }} | |||
@@ -453,6 +457,7 @@ export function initRepositoryActionView() { | |||
showTimeStamps: el.getAttribute('data-locale-show-timestamps'), | |||
showLogSeconds: el.getAttribute('data-locale-show-log-seconds'), | |||
showFullScreen: el.getAttribute('data-locale-show-full-screen'), | |||
downloadLogs: el.getAttribute('data-locale-download-logs'), | |||
status: { | |||
unknown: el.getAttribute('data-locale-status-unknown'), | |||
waiting: el.getAttribute('data-locale-status-waiting'), |
@@ -22,6 +22,7 @@ import octiconDiffModified from '../../public/img/svg/octicon-diff-modified.svg' | |||
import octiconDiffRemoved from '../../public/img/svg/octicon-diff-removed.svg'; | |||
import octiconDiffRenamed from '../../public/img/svg/octicon-diff-renamed.svg'; | |||
import octiconDotFill from '../../public/img/svg/octicon-dot-fill.svg'; | |||
import octiconDownload from '../../public/img/svg/octicon-download.svg'; | |||
import octiconEye from '../../public/img/svg/octicon-eye.svg'; | |||
import octiconFile from '../../public/img/svg/octicon-file.svg'; | |||
import octiconFileDirectoryFill from '../../public/img/svg/octicon-file-directory-fill.svg'; | |||
@@ -91,6 +92,7 @@ const svgs = { | |||
'octicon-diff-removed': octiconDiffRemoved, | |||
'octicon-diff-renamed': octiconDiffRenamed, | |||
'octicon-dot-fill': octiconDotFill, | |||
'octicon-download': octiconDownload, | |||
'octicon-eye': octiconEye, | |||
'octicon-file': octiconFile, | |||
'octicon-file-directory-fill': octiconFileDirectoryFill, |