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 }