tlog.app/go/tlog@v0.23.1/rotating/file.go (about)

     1  package rotating
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"io/fs"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  	"sync"
    11  	"sync/atomic"
    12  	"time"
    13  
    14  	"tlog.app/go/errors"
    15  )
    16  
    17  type (
    18  	File struct {
    19  		mu sync.Mutex
    20  		w  io.Writer
    21  
    22  		name            string
    23  		current         string
    24  		dir, pref, suff string
    25  		format          string
    26  		num             int
    27  
    28  		size  int64
    29  		start time.Time
    30  
    31  		MaxFileSize int64
    32  		MaxFileAge  time.Duration
    33  
    34  		MaxTotalSize  int64
    35  		MaxTotalAge   time.Duration
    36  		MaxTotalFiles int // including current
    37  
    38  		//	SubstPattern string
    39  
    40  		Flags int
    41  		Mode  os.FileMode
    42  
    43  		OpenFile FileOpener                               `deep:"compare=nil"`
    44  		readdir  func(name string) ([]fs.DirEntry, error) `deep:"-"`
    45  		fstat    func(name string) (fs.FileInfo, error)   `deep:"-"`
    46  		remove   func(name string) error                  `deep:"-"`
    47  		symlink  func(target, name string) error          `deep:"-"`
    48  
    49  		removeSingleflight atomic.Bool
    50  	}
    51  
    52  	FileOpener = func(name string, flags int, mode os.FileMode) (io.Writer, error)
    53  )
    54  
    55  const (
    56  	B = 1 << (iota * 10)
    57  	KiB
    58  	MiB
    59  	GiB
    60  	TiB
    61  
    62  	KB = 1e3
    63  	MB = 1e6
    64  	GB = 1e9
    65  	TB = 1e12
    66  )
    67  
    68  var (
    69  	//	SubstPattern = "XXXX"
    70  	//	TimeFormat   = "2006-01-02_15-04"
    71  
    72  	patterns = []string{"xxx", "XXX", "ddd"}
    73  )
    74  
    75  func Create(name string) (f *File) {
    76  	f = &File{
    77  		name: name,
    78  
    79  		MaxFileSize:   128 * MiB,
    80  		MaxTotalAge:   28 * 24 * time.Hour,
    81  		MaxTotalFiles: 10,
    82  
    83  		//	SubstPattern: SubstPattern,
    84  		//	TimeFormat: TimeFormat,
    85  
    86  		OpenFile: openFile, // OpenFileTimeSubstWithSymlink,
    87  		Flags:    os.O_CREATE | os.O_APPEND | os.O_WRONLY,
    88  		Mode:     0o644,
    89  
    90  		readdir: os.ReadDir,
    91  		fstat:   os.Stat,
    92  		remove:  os.Remove,
    93  		symlink: os.Symlink,
    94  	}
    95  
    96  	return f
    97  }
    98  
    99  func (f *File) Write(p []byte) (n int, err error) {
   100  	defer f.mu.Unlock()
   101  	f.mu.Lock()
   102  
   103  	if f.w == nil || f.size != 0 &&
   104  		(f.MaxFileSize != 0 && f.size+int64(len(p)) > f.MaxFileSize ||
   105  			f.MaxFileAge != 0 && time.Since(f.start) > f.MaxFileAge) {
   106  
   107  		err = f.rotate()
   108  		if err != nil {
   109  			return 0, errors.Wrap(err, "rotate")
   110  		}
   111  	}
   112  
   113  	n, err = f.w.Write(p)
   114  	f.size += int64(n)
   115  	if err != nil {
   116  		return
   117  	}
   118  
   119  	return
   120  }
   121  
   122  func (f *File) Rotate() error {
   123  	defer f.mu.Unlock()
   124  	f.mu.Lock()
   125  
   126  	return f.rotate()
   127  }
   128  
   129  func (f *File) rotate() (err error) {
   130  	if f.format == "" {
   131  		f.dir, f.pref, f.suff, f.format = splitPattern(f.name)
   132  
   133  		f.num, err = f.findMaxNum(f.dir, f.pref, f.suff, f.format)
   134  		if err != nil {
   135  			return errors.Wrap(err, "find max num")
   136  		}
   137  	}
   138  
   139  	f.num++
   140  	//	base := fmt.Sprintf("%s%s%s", f.pref, fmt.Sprintf(f.format, f.num), f.suff)
   141  	base := f.pref + fmt.Sprintf(f.format, f.num) + f.suff
   142  	fname := filepath.Join(f.dir, base)
   143  
   144  	if c, ok := f.w.(io.Closer); ok {
   145  		_ = c.Close()
   146  	}
   147  
   148  	f.w, err = f.OpenFile(fname, f.Flags, f.Mode)
   149  	if err != nil {
   150  		return errors.Wrap(err, "")
   151  	}
   152  
   153  	now := time.Now()
   154  
   155  	f.current = fname
   156  	f.size = 0
   157  	f.start = now
   158  	//	f.start = fileCtime(f.fstat, fname, now)
   159  
   160  	if f.symlink != nil {
   161  		link := filepath.Join(f.dir, f.pref+"LATEST"+f.suff)
   162  
   163  		_ = f.remove(link)
   164  		_ = f.symlink(base, link)
   165  	}
   166  
   167  	if f.MaxTotalSize != 0 || f.MaxTotalAge != 0 || f.MaxTotalFiles != 0 {
   168  		go f.removeOld(f.dir, base, f.pref, f.suff, f.format, f.start)
   169  	}
   170  
   171  	return
   172  }
   173  
   174  func (f *File) removeOld(dir, base, pref, suff, format string, now time.Time) error {
   175  	if f.removeSingleflight.Swap(true) {
   176  		return errors.New("already running")
   177  	}
   178  
   179  	defer f.removeSingleflight.Store(false)
   180  
   181  	files, err := f.matchingFiles(dir, pref, suff, format)
   182  	if err != nil {
   183  		return err
   184  	}
   185  
   186  	files, err = f.filesToRemove(dir, base, now, files)
   187  	if err != nil {
   188  		return err
   189  	}
   190  
   191  	for _, name := range files {
   192  		n := filepath.Join(dir, name)
   193  
   194  		e := f.remove(n)
   195  		if err == nil {
   196  			err = errors.Wrap(e, "remove %v", name)
   197  		}
   198  	}
   199  
   200  	return nil
   201  }
   202  
   203  func (f *File) findMaxNum(dir, pref, suff, format string) (num int, err error) {
   204  	entries, err := f.readdir(dir)
   205  	if err != nil {
   206  		return 0, errors.Wrap(err, "read dir")
   207  	}
   208  
   209  	for _, e := range entries {
   210  		n := e.Name()
   211  
   212  		m, ok := f.parseName(n, pref, suff, format)
   213  		if !ok {
   214  			continue
   215  		}
   216  
   217  		if m > num {
   218  			num = m
   219  		}
   220  	}
   221  
   222  	return num, nil
   223  }
   224  
   225  func (f *File) matchingFiles(dir, pref, suff, format string) ([]string, error) {
   226  	entries, err := f.readdir(dir)
   227  	if err != nil {
   228  		return nil, errors.Wrap(err, "read dir")
   229  	}
   230  
   231  	var files []string
   232  
   233  	for _, e := range entries {
   234  		n := e.Name()
   235  
   236  		_, ok := f.parseName(n, pref, suff, format)
   237  		if !ok {
   238  			continue
   239  		}
   240  
   241  		files = append(files, n)
   242  	}
   243  
   244  	return files, nil
   245  }
   246  
   247  func (f *File) parseName(n, pref, suff, format string) (num int, ok bool) {
   248  	//	defer func() { println("parse name", n, pref, suff, num, ok, loc.Caller(1).String()) }()
   249  	if !strings.HasPrefix(n, pref) || !strings.HasSuffix(n, suff) || n == pref+"LATEST"+suff {
   250  		return 0, false
   251  	}
   252  
   253  	uniq := n[len(pref) : len(n)-len(suff)]
   254  
   255  	if _, err := fmt.Sscanf(uniq, format, &num); err != nil {
   256  		return 0, false
   257  	}
   258  
   259  	return num, true
   260  }
   261  
   262  func (f *File) filesToRemove(dir, base string, now time.Time, files []string) ([]string, error) {
   263  	p := len(files)
   264  
   265  	for p > 0 && files[p-1] >= base {
   266  		p--
   267  	}
   268  
   269  	//	tlog.Printw("files to remove", "past", files[:p], "future", files[p:])
   270  
   271  	files = files[:p]
   272  	size := int64(0)
   273  
   274  	for p > 0 {
   275  		prev := p - 1
   276  
   277  		if f.MaxTotalFiles != 0 && len(files)-prev+1 > f.MaxTotalFiles {
   278  			//	tlog.Printw("remove files", "reason", "max_total_files", "x", len(files)-prev, "of", f.MaxTotalFiles, "files", files[:p])
   279  			break
   280  		}
   281  
   282  		n := filepath.Join(dir, files[prev])
   283  
   284  		inf, err := f.fstat(n)
   285  		if err != nil {
   286  			return nil, errors.Wrap(err, "stat %v", files[prev])
   287  		}
   288  
   289  		size += inf.Size()
   290  
   291  		if f.MaxTotalSize != 0 && size > f.MaxTotalSize {
   292  			//	tlog.Printw("remove files", "reason", "max_total_size", "total_size", size, "of", f.MaxTotalSize, "files", files[:p])
   293  			break
   294  		}
   295  
   296  		if f.MaxTotalAge != 0 && now.Sub(ctime(inf, now)) > f.MaxTotalAge {
   297  			//	tlog.Printw("remove files", "reason", "max_total_age", "total_age", now.Sub(ctime(inf, now)), "of", f.MaxTotalAge, "files", files[:p])
   298  			break
   299  		}
   300  
   301  		p--
   302  	}
   303  
   304  	return files[:p], nil
   305  }
   306  
   307  func (f *File) Close() (err error) {
   308  	defer f.mu.Unlock()
   309  	f.mu.Lock()
   310  
   311  	c, ok := f.w.(io.Closer)
   312  	if ok {
   313  		err = c.Close()
   314  	}
   315  
   316  	f.w = nil
   317  
   318  	return err
   319  }
   320  
   321  func IsPattern(name string) bool {
   322  	_, pos := findPattern(name)
   323  
   324  	return pos != -1
   325  }
   326  
   327  func splitPattern(name string) (dir, pref, suff, format string) {
   328  	dir = filepath.Dir(name)
   329  	base := filepath.Base(name)
   330  
   331  	pattern, pos := findPattern(base)
   332  
   333  	if pos == -1 {
   334  		suff = filepath.Ext(base)
   335  		pref = strings.TrimSuffix(base, suff)
   336  		format = "%04X"
   337  
   338  		return
   339  	}
   340  
   341  	l := len(pattern)
   342  
   343  	for pos > 0 && base[pos-1] == pattern[0] {
   344  		pos--
   345  		l++
   346  	}
   347  
   348  	pref, suff = base[:pos], base[pos+l:]
   349  	format = fmt.Sprintf("%%0%d%c", l, pattern[0])
   350  
   351  	return
   352  }
   353  
   354  func findPattern(name string) (string, int) {
   355  	var pattern string
   356  	var pos int = -1
   357  
   358  	for _, pat := range patterns {
   359  		p := strings.LastIndex(name, pat)
   360  		if p > pos {
   361  			pos = p
   362  			pattern = pat
   363  		}
   364  	}
   365  
   366  	return pattern, pos
   367  }
   368  
   369  func openFile(name string, flags int, mode os.FileMode) (io.Writer, error) {
   370  	return os.OpenFile(name, flags, mode)
   371  }