forked from gitea/gitea
		
	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:
		
							parent
							
								
									b08647f0b9
								
							
						
					
					
						commit
						f0b773e0ce
					
				| @ -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, | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Vitaliy Filippov
						Vitaliy Filippov