code.gitea.io/gitea@v1.19.3/modules/actions/log.go (about)

     1  // Copyright 2022 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package actions
     5  
     6  import (
     7  	"bufio"
     8  	"context"
     9  	"fmt"
    10  	"io"
    11  	"os"
    12  	"strings"
    13  	"time"
    14  
    15  	"code.gitea.io/gitea/models/dbfs"
    16  	"code.gitea.io/gitea/modules/log"
    17  	"code.gitea.io/gitea/modules/storage"
    18  
    19  	runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
    20  	"google.golang.org/protobuf/types/known/timestamppb"
    21  )
    22  
    23  const (
    24  	MaxLineSize = 64 * 1024
    25  	DBFSPrefix  = "actions_log/"
    26  
    27  	timeFormat     = "2006-01-02T15:04:05.0000000Z07:00"
    28  	defaultBufSize = MaxLineSize
    29  )
    30  
    31  func WriteLogs(ctx context.Context, filename string, offset int64, rows []*runnerv1.LogRow) ([]int, error) {
    32  	name := DBFSPrefix + filename
    33  	f, err := dbfs.OpenFile(ctx, name, os.O_WRONLY|os.O_CREATE)
    34  	if err != nil {
    35  		return nil, fmt.Errorf("dbfs OpenFile %q: %w", name, err)
    36  	}
    37  	defer f.Close()
    38  	if _, err := f.Seek(offset, io.SeekStart); err != nil {
    39  		return nil, fmt.Errorf("dbfs Seek %q: %w", name, err)
    40  	}
    41  
    42  	writer := bufio.NewWriterSize(f, defaultBufSize)
    43  
    44  	ns := make([]int, 0, len(rows))
    45  	for _, row := range rows {
    46  		n, err := writer.WriteString(FormatLog(row.Time.AsTime(), row.Content) + "\n")
    47  		if err != nil {
    48  			return nil, err
    49  		}
    50  		ns = append(ns, n)
    51  	}
    52  
    53  	if err := writer.Flush(); err != nil {
    54  		return nil, err
    55  	}
    56  	return ns, nil
    57  }
    58  
    59  func ReadLogs(ctx context.Context, inStorage bool, filename string, offset, limit int64) ([]*runnerv1.LogRow, error) {
    60  	f, err := openLogs(ctx, inStorage, filename)
    61  	if err != nil {
    62  		return nil, err
    63  	}
    64  	defer f.Close()
    65  
    66  	if _, err := f.Seek(offset, io.SeekStart); err != nil {
    67  		return nil, fmt.Errorf("file seek: %w", err)
    68  	}
    69  
    70  	scanner := bufio.NewScanner(f)
    71  	maxLineSize := len(timeFormat) + MaxLineSize + 1
    72  	scanner.Buffer(make([]byte, maxLineSize), maxLineSize)
    73  
    74  	var rows []*runnerv1.LogRow
    75  	for scanner.Scan() && (int64(len(rows)) < limit || limit < 0) {
    76  		t, c, err := ParseLog(scanner.Text())
    77  		if err != nil {
    78  			return nil, fmt.Errorf("parse log %q: %w", scanner.Text(), err)
    79  		}
    80  		rows = append(rows, &runnerv1.LogRow{
    81  			Time:    timestamppb.New(t),
    82  			Content: c,
    83  		})
    84  	}
    85  
    86  	if err := scanner.Err(); err != nil {
    87  		return nil, fmt.Errorf("scan: %w", err)
    88  	}
    89  
    90  	return rows, nil
    91  }
    92  
    93  func TransferLogs(ctx context.Context, filename string) (func(), error) {
    94  	name := DBFSPrefix + filename
    95  	remove := func() {
    96  		if err := dbfs.Remove(ctx, name); err != nil {
    97  			log.Warn("dbfs remove %q: %v", name, err)
    98  		}
    99  	}
   100  	f, err := dbfs.Open(ctx, name)
   101  	if err != nil {
   102  		return nil, fmt.Errorf("dbfs open %q: %w", name, err)
   103  	}
   104  	defer f.Close()
   105  
   106  	if _, err := storage.Actions.Save(filename, f, -1); err != nil {
   107  		return nil, fmt.Errorf("storage save %q: %w", filename, err)
   108  	}
   109  	return remove, nil
   110  }
   111  
   112  func RemoveLogs(ctx context.Context, inStorage bool, filename string) error {
   113  	if !inStorage {
   114  		name := DBFSPrefix + filename
   115  		err := dbfs.Remove(ctx, name)
   116  		if err != nil {
   117  			return fmt.Errorf("dbfs remove %q: %w", name, err)
   118  		}
   119  		return nil
   120  	}
   121  	err := storage.Actions.Delete(filename)
   122  	if err != nil {
   123  		return fmt.Errorf("storage delete %q: %w", filename, err)
   124  	}
   125  	return nil
   126  }
   127  
   128  func openLogs(ctx context.Context, inStorage bool, filename string) (io.ReadSeekCloser, error) {
   129  	if !inStorage {
   130  		name := DBFSPrefix + filename
   131  		f, err := dbfs.Open(ctx, name)
   132  		if err != nil {
   133  			return nil, fmt.Errorf("dbfs open %q: %w", name, err)
   134  		}
   135  		return f, nil
   136  	}
   137  	f, err := storage.Actions.Open(filename)
   138  	if err != nil {
   139  		return nil, fmt.Errorf("storage open %q: %w", filename, err)
   140  	}
   141  	return f, nil
   142  }
   143  
   144  func FormatLog(timestamp time.Time, content string) string {
   145  	// Content shouldn't contain new line, it will break log indexes, other control chars are safe.
   146  	content = strings.ReplaceAll(content, "\n", `\n`)
   147  	if len(content) > MaxLineSize {
   148  		content = content[:MaxLineSize]
   149  	}
   150  	return fmt.Sprintf("%s %s", timestamp.UTC().Format(timeFormat), content)
   151  }
   152  
   153  func ParseLog(in string) (time.Time, string, error) {
   154  	index := strings.IndexRune(in, ' ')
   155  	if index < 0 {
   156  		return time.Time{}, "", fmt.Errorf("invalid log: %q", in)
   157  	}
   158  	timestamp, err := time.Parse(timeFormat, in[:index])
   159  	if err != nil {
   160  		return time.Time{}, "", err
   161  	}
   162  	return timestamp, in[index+1:], nil
   163  }