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  }