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 }