gitech

Форк
0
246 строк · 5.5 Кб
1
// Copyright 2023 The Gitea Authors. All rights reserved.
2
// SPDX-License-Identifier: MIT
3

4
package rotatingfilewriter
5

6
import (
7
	"bufio"
8
	"compress/gzip"
9
	"errors"
10
	"fmt"
11
	"os"
12
	"path/filepath"
13
	"strings"
14
	"sync"
15
	"time"
16

17
	"code.gitea.io/gitea/modules/graceful/releasereopen"
18
	"code.gitea.io/gitea/modules/util"
19
)
20

21
type Options struct {
22
	Rotate           bool
23
	MaximumSize      int64
24
	RotateDaily      bool
25
	KeepDays         int
26
	Compress         bool
27
	CompressionLevel int
28
}
29

30
type RotatingFileWriter struct {
31
	mu sync.Mutex
32
	fd *os.File
33

34
	currentSize int64
35
	openDate    int
36

37
	options Options
38

39
	cancelReleaseReopen func()
40
}
41

42
var ErrorPrintf func(format string, args ...any)
43

44
// errorf tries to print error messages. Since this writer could be used by a logger system, this is the last chance to show the error in some cases
45
func errorf(format string, args ...any) {
46
	if ErrorPrintf != nil {
47
		ErrorPrintf("rotatingfilewriter: "+format+"\n", args...)
48
	}
49
}
50

51
// Open creates a new rotating file writer.
52
// Notice: if a file is opened by two rotators, there will be conflicts when rotating.
53
// In the future, there should be "rotating file manager"
54
func Open(filename string, options *Options) (*RotatingFileWriter, error) {
55
	if options == nil {
56
		options = &Options{}
57
	}
58

59
	rfw := &RotatingFileWriter{
60
		options: *options,
61
	}
62

63
	if err := rfw.open(filename); err != nil {
64
		return nil, err
65
	}
66

67
	rfw.cancelReleaseReopen = releasereopen.GetManager().Register(rfw)
68
	return rfw, nil
69
}
70

71
func (rfw *RotatingFileWriter) Write(b []byte) (int, error) {
72
	if rfw.options.Rotate && ((rfw.options.MaximumSize > 0 && rfw.currentSize >= rfw.options.MaximumSize) || (rfw.options.RotateDaily && time.Now().Day() != rfw.openDate)) {
73
		if err := rfw.DoRotate(); err != nil {
74
			// if this writer is used by a logger system, it's the logger system's responsibility to handle/show the error
75
			return 0, err
76
		}
77
	}
78

79
	n, err := rfw.fd.Write(b)
80
	if err == nil {
81
		rfw.currentSize += int64(n)
82
	}
83
	return n, err
84
}
85

86
func (rfw *RotatingFileWriter) Flush() error {
87
	return rfw.fd.Sync()
88
}
89

90
func (rfw *RotatingFileWriter) Close() error {
91
	rfw.mu.Lock()
92
	if rfw.cancelReleaseReopen != nil {
93
		rfw.cancelReleaseReopen()
94
		rfw.cancelReleaseReopen = nil
95
	}
96
	rfw.mu.Unlock()
97
	return rfw.fd.Close()
98
}
99

100
func (rfw *RotatingFileWriter) open(filename string) error {
101
	fd, err := os.OpenFile(filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o660)
102
	if err != nil {
103
		return err
104
	}
105

106
	rfw.fd = fd
107

108
	finfo, err := fd.Stat()
109
	if err != nil {
110
		return err
111
	}
112
	rfw.currentSize = finfo.Size()
113
	rfw.openDate = finfo.ModTime().Day()
114

115
	return nil
116
}
117

118
func (rfw *RotatingFileWriter) ReleaseReopen() error {
119
	return errors.Join(
120
		rfw.fd.Close(),
121
		rfw.open(rfw.fd.Name()),
122
	)
123
}
124

125
// DoRotate the log file creating a backup like xx.2013-01-01.2
126
func (rfw *RotatingFileWriter) DoRotate() error {
127
	if !rfw.options.Rotate {
128
		return nil
129
	}
130

131
	rfw.mu.Lock()
132
	defer rfw.mu.Unlock()
133

134
	prefix := fmt.Sprintf("%s.%s.", rfw.fd.Name(), time.Now().Format("2006-01-02"))
135

136
	var err error
137
	fname := ""
138
	for i := 1; err == nil && i <= 999; i++ {
139
		fname = prefix + fmt.Sprintf("%03d", i)
140
		_, err = os.Lstat(fname)
141
		if rfw.options.Compress && err != nil {
142
			_, err = os.Lstat(fname + ".gz")
143
		}
144
	}
145
	// return error if the last file checked still existed
146
	if err == nil {
147
		return fmt.Errorf("cannot find free file to rename %s", rfw.fd.Name())
148
	}
149

150
	fd := rfw.fd
151
	if err := fd.Close(); err != nil { // close file before rename
152
		return err
153
	}
154

155
	if err := util.Rename(fd.Name(), fname); err != nil {
156
		return err
157
	}
158

159
	if rfw.options.Compress {
160
		go func() {
161
			err := compressOldFile(fname, rfw.options.CompressionLevel)
162
			if err != nil {
163
				errorf("DoRotate: %v", err)
164
			}
165
		}()
166
	}
167

168
	if err := rfw.open(fd.Name()); err != nil {
169
		return err
170
	}
171

172
	go deleteOldFiles(
173
		filepath.Dir(fd.Name()),
174
		filepath.Base(fd.Name()),
175
		time.Now().AddDate(0, 0, -rfw.options.KeepDays),
176
	)
177

178
	return nil
179
}
180

181
func compressOldFile(fname string, compressionLevel int) error {
182
	reader, err := os.Open(fname)
183
	if err != nil {
184
		return fmt.Errorf("compressOldFile: failed to open existing file %s: %w", fname, err)
185
	}
186
	defer reader.Close()
187

188
	buffer := bufio.NewReader(reader)
189
	fnameGz := fname + ".gz"
190
	fw, err := os.OpenFile(fnameGz, os.O_WRONLY|os.O_CREATE, 0o660)
191
	if err != nil {
192
		return fmt.Errorf("compressOldFile: failed to open new file %s: %w", fnameGz, err)
193
	}
194
	defer fw.Close()
195

196
	zw, err := gzip.NewWriterLevel(fw, compressionLevel)
197
	if err != nil {
198
		return fmt.Errorf("compressOldFile: failed to create gzip writer: %w", err)
199
	}
200
	defer zw.Close()
201

202
	_, err = buffer.WriteTo(zw)
203
	if err != nil {
204
		_ = zw.Close()
205
		_ = fw.Close()
206
		_ = util.Remove(fname + ".gz")
207
		return fmt.Errorf("compressOldFile: failed to write to gz file: %w", err)
208
	}
209
	_ = reader.Close()
210

211
	err = util.Remove(fname)
212
	if err != nil {
213
		return fmt.Errorf("compressOldFile: failed to delete old file: %w", err)
214
	}
215
	return nil
216
}
217

218
func deleteOldFiles(dir, prefix string, removeBefore time.Time) {
219
	err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) (returnErr error) {
220
		defer func() {
221
			if r := recover(); r != nil {
222
				returnErr = fmt.Errorf("unable to delete old file '%s', error: %+v", path, r)
223
			}
224
		}()
225

226
		if err != nil {
227
			return err
228
		}
229
		if d.IsDir() {
230
			return nil
231
		}
232
		info, err := d.Info()
233
		if err != nil {
234
			return err
235
		}
236
		if info.ModTime().Before(removeBefore) {
237
			if strings.HasPrefix(filepath.Base(path), prefix) {
238
				return util.Remove(path)
239
			}
240
		}
241
		return nil
242
	})
243
	if err != nil {
244
		errorf("deleteOldFiles: failed to delete old file: %v", err)
245
	}
246
}
247

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.