code.gitea.io/gitea@v1.22.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  	flag := os.O_WRONLY
    33  	if offset == 0 {
    34  		// Create file only if offset is 0, or it could result in content holes if the file doesn't exist.
    35  		flag |= os.O_CREATE
    36  	}
    37  	name := DBFSPrefix + filename
    38  	f, err := dbfs.OpenFile(ctx, name, flag)
    39  	if err != nil {
    40  		return nil, fmt.Errorf("dbfs OpenFile %q: %w", name, err)
    41  	}
    42  	defer f.Close()
    43  
    44  	stat, err := f.Stat()
    45  	if err != nil {
    46  		return nil, fmt.Errorf("dbfs Stat %q: %w", name, err)
    47  	}
    48  	if stat.Size() < offset {
    49  		// If the size is less than offset, refuse to write, or it could result in content holes.
    50  		// However, if the size is greater than offset, we can still write to overwrite the content.
    51  		return nil, fmt.Errorf("size of %q is less than offset", name)
    52  	}
    53  
    54  	if _, err := f.Seek(offset, io.SeekStart); err != nil {
    55  		return nil, fmt.Errorf("dbfs Seek %q: %w", name, err)
    56  	}
    57  
    58  	writer := bufio.NewWriterSize(f, defaultBufSize)
    59  
    60  	ns := make([]int, 0, len(rows))
    61  	for _, row := range rows {
    62  		n, err := writer.WriteString(FormatLog(row.Time.AsTime(), row.Content) + "\n")
    63  		if err != nil {
    64  			return nil, err
    65  		}
    66  		ns = append(ns, n)
    67  	}
    68  
    69  	if err := writer.Flush(); err != nil {
    70  		return nil, err
    71  	}
    72  	return ns, nil
    73  }
    74  
    75  func ReadLogs(ctx context.Context, inStorage bool, filename string, offset, limit int64) ([]*runnerv1.LogRow, error) {
    76  	f, err := OpenLogs(ctx, inStorage, filename)
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  	defer f.Close()
    81  
    82  	if _, err := f.Seek(offset, io.SeekStart); err != nil {
    83  		return nil, fmt.Errorf("file seek: %w", err)
    84  	}
    85  
    86  	scanner := bufio.NewScanner(f)
    87  	maxLineSize := len(timeFormat) + MaxLineSize + 1
    88  	scanner.Buffer(make([]byte, maxLineSize), maxLineSize)
    89  
    90  	var rows []*runnerv1.LogRow
    91  	for scanner.Scan() && (int64(len(rows)) < limit || limit < 0) {
    92  		t, c, err := ParseLog(scanner.Text())
    93  		if err != nil {
    94  			return nil, fmt.Errorf("parse log %q: %w", scanner.Text(), err)
    95  		}
    96  		rows = append(rows, &runnerv1.LogRow{
    97  			Time:    timestamppb.New(t),
    98  			Content: c,
    99  		})
   100  	}
   101  
   102  	if err := scanner.Err(); err != nil {
   103  		return nil, fmt.Errorf("ReadLogs scan: %w", err)
   104  	}
   105  
   106  	return rows, nil
   107  }
   108  
   109  func TransferLogs(ctx context.Context, filename string) (func(), error) {
   110  	name := DBFSPrefix + filename
   111  	remove := func() {
   112  		if err := dbfs.Remove(ctx, name); err != nil {
   113  			log.Warn("dbfs remove %q: %v", name, err)
   114  		}
   115  	}
   116  	f, err := dbfs.Open(ctx, name)
   117  	if err != nil {
   118  		return nil, fmt.Errorf("dbfs open %q: %w", name, err)
   119  	}
   120  	defer f.Close()
   121  
   122  	if _, err := storage.Actions.Save(filename, f, -1); err != nil {
   123  		return nil, fmt.Errorf("storage save %q: %w", filename, err)
   124  	}
   125  	return remove, nil
   126  }
   127  
   128  func RemoveLogs(ctx context.Context, inStorage bool, filename string) error {
   129  	if !inStorage {
   130  		name := DBFSPrefix + filename
   131  		err := dbfs.Remove(ctx, name)
   132  		if err != nil {
   133  			return fmt.Errorf("dbfs remove %q: %w", name, err)
   134  		}
   135  		return nil
   136  	}
   137  	err := storage.Actions.Delete(filename)
   138  	if err != nil {
   139  		return fmt.Errorf("storage delete %q: %w", filename, err)
   140  	}
   141  	return nil
   142  }
   143  
   144  func OpenLogs(ctx context.Context, inStorage bool, filename string) (io.ReadSeekCloser, error) {
   145  	if !inStorage {
   146  		name := DBFSPrefix + filename
   147  		f, err := dbfs.Open(ctx, name)
   148  		if err != nil {
   149  			return nil, fmt.Errorf("dbfs open %q: %w", name, err)
   150  		}
   151  		return f, nil
   152  	}
   153  	f, err := storage.Actions.Open(filename)
   154  	if err != nil {
   155  		return nil, fmt.Errorf("storage open %q: %w", filename, err)
   156  	}
   157  	return f, nil
   158  }
   159  
   160  func FormatLog(timestamp time.Time, content string) string {
   161  	// Content shouldn't contain new line, it will break log indexes, other control chars are safe.
   162  	content = strings.ReplaceAll(content, "\n", `\n`)
   163  	if len(content) > MaxLineSize {
   164  		content = content[:MaxLineSize]
   165  	}
   166  	return fmt.Sprintf("%s %s", timestamp.UTC().Format(timeFormat), content)
   167  }
   168  
   169  func ParseLog(in string) (time.Time, string, error) {
   170  	index := strings.IndexRune(in, ' ')
   171  	if index < 0 {
   172  		return time.Time{}, "", fmt.Errorf("invalid log: %q", in)
   173  	}
   174  	timestamp, err := time.Parse(timeFormat, in[:index])
   175  	if err != nil {
   176  		return time.Time{}, "", err
   177  	}
   178  	return timestamp, in[index+1:], nil
   179  }