github.com/bingoohuang/gg@v0.0.0-20240325092523-45da7dee9335/pkg/rotate/fn.go (about) 1 package rotate 2 3 import ( 4 "bufio" 5 "compress/gzip" 6 "fmt" 7 "io" 8 "os" 9 "path/filepath" 10 "regexp" 11 "strconv" 12 "strings" 13 "time" 14 15 "github.com/bingoohuang/gg/pkg/timex" 16 ) 17 18 // bufWriter is a Writer interface that also has a Flush method. 19 type bufWriter interface { 20 io.Writer 21 io.Closer 22 Flush() error 23 } 24 25 type FileWriter struct { 26 FnTemplate string 27 MaxSize uint64 28 Append bool 29 30 file *os.File 31 curFn string 32 curSize uint64 33 rotateFunc func() bool 34 writer bufWriter 35 DotGz string 36 maxIndex int 37 timedFn string 38 MaxKeepDays int 39 } 40 41 func NewFileWriter(fnTemplate string, maxSize uint64, append bool, maxKeepDays int) *FileWriter { 42 hasGz := strings.HasSuffix(fnTemplate, ".gz") 43 dotGz := "" 44 if hasGz { 45 dotGz = ".gz" 46 fnTemplate = strings.TrimSuffix(fnTemplate, ".gz") 47 } 48 r := &FileWriter{ 49 FnTemplate: fnTemplate, 50 DotGz: dotGz, 51 MaxSize: maxSize, 52 Append: append, 53 rotateFunc: func() bool { return false }, 54 MaxKeepDays: maxKeepDays, 55 } 56 57 if r.MaxSize > 0 { 58 r.rotateFunc = func() bool { return r.curSize >= r.MaxSize } 59 } 60 61 return r 62 } 63 64 func (w *FileWriter) daysKeeping() { 65 expired := time.Now().Add(time.Duration(w.MaxKeepDays) * -24 * time.Hour) 66 matches, _ := filepath.Glob(matchExpiredFiles(w.FnTemplate, w.DotGz)) 67 for _, f := range matches { 68 if stat, _ := os.Stat(f); stat != nil && stat.ModTime().Before(expired) { 69 _ = os.Remove(f) 70 } 71 } 72 } 73 74 func matchExpiredFiles(fnTemplate, dotGz string) string { 75 fn := timex.GlobName(fnTemplate) 76 fn = filepath.Clean(fn) 77 base, _, ext := SplitBaseIndexExt(fn) 78 return base + "*" + ext + dotGz 79 } 80 81 func (w *FileWriter) Write(p []byte) (int, error) { 82 timedFn := w.NewTimedFilename(w.FnTemplate, w.DotGz) 83 84 for { 85 fn := w.RotateFilename(timedFn) 86 if fn == w.curFn { 87 break 88 } 89 90 if ok, err := w.openFile(fn); err != nil { 91 return 0, err 92 } else if ok { 93 break 94 } 95 } 96 97 n, err := w.writer.Write(p) 98 w.curSize += uint64(n) 99 return n, err 100 } 101 102 type gzipWriter struct { 103 Buf *bufio.Writer 104 *gzip.Writer 105 } 106 107 func (w *gzipWriter) Close() error { return w.Writer.Close() } 108 func (w *gzipWriter) Flush() error { return w.Buf.Flush() } 109 110 type bufioWriter struct { 111 *bufio.Writer 112 } 113 114 func (b *bufioWriter) Close() error { return b.Writer.Flush() } 115 func (b *bufioWriter) Flush() error { return b.Writer.Flush() } 116 117 func (w *FileWriter) openFile(fn string) (ok bool, err error) { 118 _ = w.Close() 119 if w.maxIndex == 2 { // rename bbb-2021-05-27-18-26.http to bbb-2021-05-27-18-26_00001.http 120 _ = os.Rename(w.curFn+w.DotGz, SetFileIndex(w.curFn, 1)+w.DotGz) 121 } 122 123 w.file, err = os.OpenFile(fn+w.DotGz, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o660) 124 if err != nil { 125 return false, err 126 } 127 128 w.curFn = fn 129 130 if w.DotGz != "" { 131 gw := gzip.NewWriter(w.file) 132 w.writer = &gzipWriter{Buf: bufio.NewWriter(gw), Writer: gw} 133 } else { 134 w.writer = &bufioWriter{Writer: bufio.NewWriter(w.file)} 135 } 136 137 ok = true 138 139 if stat, _ := w.file.Stat(); stat != nil { 140 if w.curSize = uint64(stat.Size()); w.curSize > 0 { 141 ok = !w.rotateFunc() 142 } 143 } 144 145 return ok, nil 146 } 147 148 type Flusher interface { 149 Flush() error 150 } 151 152 func (w *FileWriter) Flush() error { 153 if w.writer != nil { 154 return w.writer.Flush() 155 } 156 157 return nil 158 } 159 160 func (w *FileWriter) Close() error { 161 if w.writer != nil && w.file != nil { 162 _ = w.writer.Close() 163 _ = w.file.Close() 164 w.writer = nil 165 w.file = nil 166 167 if w.MaxKeepDays > 0 { 168 go w.daysKeeping() 169 } 170 } 171 return nil 172 } 173 174 func (w *FileWriter) NewTimedFilename(template, dotGz string) string { 175 fn := timex.FormatTime(time.Now(), template) 176 fn = filepath.Clean(fn) 177 178 if w.timedFn != fn { 179 w.maxIndex = 1 180 w.timedFn = fn 181 } 182 183 if w.curFn == "" { // 只有第一次检查最大文件索引号 184 w.maxIndex, fn = FindMaxFileIndex(fn, dotGz) 185 } 186 187 return fn 188 } 189 190 func (w *FileWriter) RotateFilename(fn string) string { 191 if w.rotateFunc() { 192 w.maxIndex++ 193 } 194 195 if w.maxIndex == 1 { 196 return fn 197 } 198 return SetFileIndex(fn, w.maxIndex) 199 } 200 201 func GetFileIndex(path string) int { 202 _, index, _ := SplitBaseIndexExt(path) 203 if index == "" { 204 return -1 205 } 206 207 v, _ := strconv.Atoi(index) 208 return v 209 } 210 211 func SetFileIndex(path string, index int) string { 212 base, _, ext := SplitBaseIndexExt(path) 213 return fmt.Sprintf("%s_%05d%s", base, index, ext) 214 } 215 216 // FindMaxFileIndex finds the maxIndex index of a file like log-2021-05-27_00001.log. 217 func FindMaxFileIndex(path string, dotGz string) (int, string) { 218 base, _, ext := SplitBaseIndexExt(path) 219 matches, _ := filepath.Glob(base + "*" + ext + dotGz) 220 maxIndex := 1 221 maxFn := path 222 for _, fn := range matches { 223 fn = strings.TrimSuffix(fn, dotGz) 224 if index := GetFileIndex(fn); index > maxIndex { 225 maxIndex = index 226 maxFn = fn 227 } 228 } 229 230 return maxIndex, maxFn 231 } 232 233 var idx = regexp.MustCompile(`_\d{5,}`) 234 235 func SplitBaseIndexExt(path string) (base, index, ext string) { 236 if subs := idx.FindAllStringSubmatchIndex(path, -1); len(subs) > 0 { 237 sub := subs[len(subs)-1] 238 return path[:sub[0]], path[sub[0]+1 : sub[1]], path[sub[1]:] 239 } 240 241 ext = filepath.Ext(path) 242 return strings.TrimSuffix(path, ext), "", ext 243 }