mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08:00 
			
		
		
		
	Support downloading raw task logs (#24451)
Hi! This pull request adds support for downloading raw task logs for Gitea Actions, similar to Github Actions It looks like the following: 
This commit is contained in:
		@@ -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) {
 | 
					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 {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -141,7 +141,7 @@ func RemoveLogs(ctx context.Context, inStorage bool, filename string) error {
 | 
				
			|||||||
	return nil
 | 
						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 {
 | 
						if !inStorage {
 | 
				
			||||||
		name := DBFSPrefix + filename
 | 
							name := DBFSPrefix + filename
 | 
				
			||||||
		f, err := dbfs.Open(ctx, name)
 | 
							f, err := dbfs.Open(ctx, name)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -129,6 +129,7 @@ concept_user_organization = Organization
 | 
				
			|||||||
show_timestamps = Show timestamps
 | 
					show_timestamps = Show timestamps
 | 
				
			||||||
show_log_seconds = Show seconds
 | 
					show_log_seconds = Show seconds
 | 
				
			||||||
show_full_screen = Show full screen
 | 
					show_full_screen = Show full screen
 | 
				
			||||||
 | 
					download_logs = Download logs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
confirm_delete_selected = Confirm to delete all selected items?
 | 
					confirm_delete_selected = Confirm to delete all selected items?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,7 @@ import (
 | 
				
			|||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	actions_model "code.gitea.io/gitea/models/actions"
 | 
						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
 | 
						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) {
 | 
					func Cancel(ctx *context_module.Context) {
 | 
				
			||||||
	runIndex := ctx.ParamsInt64("run")
 | 
						runIndex := ctx.ParamsInt64("run")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1207,6 +1207,7 @@ func registerRoutes(m *web.Route) {
 | 
				
			|||||||
						Get(actions.View).
 | 
											Get(actions.View).
 | 
				
			||||||
						Post(web.Bind(actions.ViewRequest{}), actions.ViewPost)
 | 
											Post(web.Bind(actions.ViewRequest{}), actions.ViewPost)
 | 
				
			||||||
					m.Post("/rerun", reqRepoActionsWriter, actions.RerunOne)
 | 
										m.Post("/rerun", reqRepoActionsWriter, actions.RerunOne)
 | 
				
			||||||
 | 
										m.Get("/logs", actions.Logs)
 | 
				
			||||||
				})
 | 
									})
 | 
				
			||||||
				m.Post("/cancel", reqRepoActionsWriter, actions.Cancel)
 | 
									m.Post("/cancel", reqRepoActionsWriter, actions.Cancel)
 | 
				
			||||||
				m.Post("/approve", reqRepoActionsWriter, actions.Approve)
 | 
									m.Post("/approve", reqRepoActionsWriter, actions.Approve)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,6 +22,7 @@
 | 
				
			|||||||
		data-locale-show-timestamps="{{.locale.Tr "show_timestamps"}}"
 | 
							data-locale-show-timestamps="{{.locale.Tr "show_timestamps"}}"
 | 
				
			||||||
		data-locale-show-log-seconds="{{.locale.Tr "show_log_seconds"}}"
 | 
							data-locale-show-log-seconds="{{.locale.Tr "show_log_seconds"}}"
 | 
				
			||||||
		data-locale-show-full-screen="{{.locale.Tr "show_full_screen"}}"
 | 
							data-locale-show-full-screen="{{.locale.Tr "show_full_screen"}}"
 | 
				
			||||||
 | 
							data-locale-download-logs="{{.locale.Tr "download_logs"}}"
 | 
				
			||||||
	>
 | 
						>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -74,6 +74,10 @@
 | 
				
			|||||||
                <SvgIcon name="octicon-gear" :size="18"/>
 | 
					                <SvgIcon name="octicon-gear" :size="18"/>
 | 
				
			||||||
              </button>
 | 
					              </button>
 | 
				
			||||||
              <div class="menu transition action-job-menu" :class="{visible: menuVisible}" v-if="menuVisible" v-cloak>
 | 
					              <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')">
 | 
					                <a class="item" @click="toggleTimeDisplay('seconds')">
 | 
				
			||||||
                  <i class="icon"><SvgIcon v-show="timeVisible['log-time-seconds']" name="octicon-check"/></i>
 | 
					                  <i class="icon"><SvgIcon v-show="timeVisible['log-time-seconds']" name="octicon-check"/></i>
 | 
				
			||||||
                  {{ locale.showLogSeconds }}
 | 
					                  {{ locale.showLogSeconds }}
 | 
				
			||||||
@@ -453,6 +457,7 @@ export function initRepositoryActionView() {
 | 
				
			|||||||
      showTimeStamps: el.getAttribute('data-locale-show-timestamps'),
 | 
					      showTimeStamps: el.getAttribute('data-locale-show-timestamps'),
 | 
				
			||||||
      showLogSeconds: el.getAttribute('data-locale-show-log-seconds'),
 | 
					      showLogSeconds: el.getAttribute('data-locale-show-log-seconds'),
 | 
				
			||||||
      showFullScreen: el.getAttribute('data-locale-show-full-screen'),
 | 
					      showFullScreen: el.getAttribute('data-locale-show-full-screen'),
 | 
				
			||||||
 | 
					      downloadLogs: el.getAttribute('data-locale-download-logs'),
 | 
				
			||||||
      status: {
 | 
					      status: {
 | 
				
			||||||
        unknown: el.getAttribute('data-locale-status-unknown'),
 | 
					        unknown: el.getAttribute('data-locale-status-unknown'),
 | 
				
			||||||
        waiting: el.getAttribute('data-locale-status-waiting'),
 | 
					        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 octiconDiffRemoved from '../../public/img/svg/octicon-diff-removed.svg';
 | 
				
			||||||
import octiconDiffRenamed from '../../public/img/svg/octicon-diff-renamed.svg';
 | 
					import octiconDiffRenamed from '../../public/img/svg/octicon-diff-renamed.svg';
 | 
				
			||||||
import octiconDotFill from '../../public/img/svg/octicon-dot-fill.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 octiconEye from '../../public/img/svg/octicon-eye.svg';
 | 
				
			||||||
import octiconFile from '../../public/img/svg/octicon-file.svg';
 | 
					import octiconFile from '../../public/img/svg/octicon-file.svg';
 | 
				
			||||||
import octiconFileDirectoryFill from '../../public/img/svg/octicon-file-directory-fill.svg';
 | 
					import octiconFileDirectoryFill from '../../public/img/svg/octicon-file-directory-fill.svg';
 | 
				
			||||||
@@ -91,6 +92,7 @@ const svgs = {
 | 
				
			|||||||
  'octicon-diff-removed': octiconDiffRemoved,
 | 
					  'octicon-diff-removed': octiconDiffRemoved,
 | 
				
			||||||
  'octicon-diff-renamed': octiconDiffRenamed,
 | 
					  'octicon-diff-renamed': octiconDiffRenamed,
 | 
				
			||||||
  'octicon-dot-fill': octiconDotFill,
 | 
					  'octicon-dot-fill': octiconDotFill,
 | 
				
			||||||
 | 
					  'octicon-download': octiconDownload,
 | 
				
			||||||
  'octicon-eye': octiconEye,
 | 
					  'octicon-eye': octiconEye,
 | 
				
			||||||
  'octicon-file': octiconFile,
 | 
					  'octicon-file': octiconFile,
 | 
				
			||||||
  'octicon-file-directory-fill': octiconFileDirectoryFill,
 | 
					  'octicon-file-directory-fill': octiconFileDirectoryFill,
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user