github.com/nikandfor/tlog@v0.21.5-0.20231108111739-3ef89426a96d/rotated/file.go (about)

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