github.com/searKing/golang/go@v1.2.117/os/file_rotate.go (about)

     1  // Copyright 2021 The searKing Author. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package os
     6  
     7  import (
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"os"
    12  	"regexp"
    13  	"sort"
    14  	"strings"
    15  	"sync"
    16  	"time"
    17  
    18  	filepath_ "github.com/searKing/golang/go/path/filepath"
    19  	"github.com/searKing/golang/go/sync/atomic"
    20  	time_ "github.com/searKing/golang/go/time"
    21  )
    22  
    23  // RotateMode represents a way to rotate file.
    24  // see details: https://man7.org/linux/man-pages/man8/logrotate.8.html
    25  type RotateMode int
    26  
    27  const (
    28  	// RotateModeNew create new rotate file directly
    29  	// Immediately after rotation (before the postrotate script is run) the log file is created (with
    30  	// the same name as the log file just rotated).
    31  	// see option: `create` in https://man7.org/linux/man-pages/man8/logrotate.8.html
    32  	RotateModeNew RotateMode = iota
    33  
    34  	// RotateModeCopyRename Make a copy of the log file, but don't change the original at all. This option can be
    35  	// used, for instance, to make a snapshot of the current log file, or when some other
    36  	// utility needs to truncate or parse the file. When this option is used, the create
    37  	// option will have no effect, as the old log file stays in place.
    38  	// see option: `copy` in https://man7.org/linux/man-pages/man8/logrotate.8.html
    39  	RotateModeCopyRename RotateMode = iota
    40  
    41  	// RotateModeCopyTruncate Truncate the original log file in place after creating a copy, instead of moving the
    42  	// old log file and optionally creating a new one. It can be used when some program can‐
    43  	// not be told to close its rotatefile and thus might continue writing (appending) to the
    44  	// previous log file forever. Note that there is a very small time slice between copying
    45  	// the file and truncating it, so some logging data might be lost. When this option is
    46  	// used, the create option will have no effect, as the old log file stays in place.
    47  	// see option: `copytruncate` in https://man7.org/linux/man-pages/man8/logrotate.8.html
    48  	RotateModeCopyTruncate RotateMode = iota
    49  )
    50  
    51  // RotateFile logrotate reads everything about the log files it should be handling from the series of con‐
    52  // figuration files specified on the command line.  Each configuration file can set global
    53  // options (local definitions override global ones, and later definitions override earlier ones)
    54  // and specify rotatefiles to rotate. A simple configuration file looks like this:
    55  type RotateFile struct {
    56  	RotateMode           RotateMode
    57  	FilePathPrefix       string // FilePath = FilePathPrefix + now.Format(filePathRotateLayout)
    58  	FilePathRotateLayout string // Time layout to format rotate file
    59  
    60  	RotateFileGlob string // file glob to clean
    61  
    62  	// sets the symbolic link name that gets linked to the current file name being used.
    63  	FileLinkPath string
    64  
    65  	// Rotate files are rotated until RotateInterval expired before being removed
    66  	// take effects if only RotateInterval is bigger than 0.
    67  	RotateInterval time.Duration
    68  
    69  	// Rotate files are rotated if they grow bigger then size bytes.
    70  	// take effects if only RotateSize is bigger than 0.
    71  	RotateSize int64
    72  
    73  	// max age of a log file before it gets purged from the file system.
    74  	// Remove rotated logs older than duration. The age is only checked if the file is
    75  	// to be rotated.
    76  	// take effects if only MaxAge is bigger than 0.
    77  	MaxAge time.Duration
    78  
    79  	// Rotate files are rotated MaxCount times before being removed
    80  	// take effects if only MaxCount is bigger than 0.
    81  	MaxCount int
    82  
    83  	// Force File Rotate when start up
    84  	ForceNewFileOnStartup bool
    85  
    86  	// PreRotateHandler called before file rotate
    87  	// name means file path rotated
    88  	PreRotateHandler func(name string)
    89  
    90  	// PostRotateHandler called after file rotate
    91  	// name means file path rotated
    92  	PostRotateHandler func(name string)
    93  
    94  	cleaning      atomic.Bool
    95  	mu            sync.Mutex
    96  	usingSeq      int // file rotated by size limit meet
    97  	usingFilePath string
    98  	usingFile     *os.File
    99  }
   100  
   101  func NewRotateFile(layout string) *RotateFile {
   102  	return NewRotateFileWithStrftime(time_.LayoutTimeToSimilarStrftime(layout))
   103  }
   104  
   105  func NewRotateFileWithStrftime(strftimeLayout string) *RotateFile {
   106  	return &RotateFile{
   107  		FilePathRotateLayout: time_.LayoutStrftimeToSimilarTime(strftimeLayout),
   108  		RotateFileGlob:       fileGlobFromStrftimeLayout(strftimeLayout),
   109  		RotateInterval:       24 * time.Hour,
   110  	}
   111  }
   112  
   113  func fileGlobFromStrftimeLayout(strftimeLayout string) string {
   114  	var regexps = []*regexp.Regexp{
   115  		regexp.MustCompile(`%[%+A-Za-z]`),
   116  		regexp.MustCompile(`\*+`),
   117  	}
   118  	globPattern := strftimeLayout
   119  	for _, re := range regexps {
   120  		globPattern = re.ReplaceAllString(globPattern, "*")
   121  	}
   122  	return globPattern + `*`
   123  }
   124  
   125  func (f *RotateFile) Write(b []byte) (n int, err error) {
   126  	if err := f.checkValid("write"); err != nil {
   127  		return 0, err
   128  	}
   129  	// Guard against concurrent writes
   130  	f.mu.Lock()
   131  	defer f.mu.Unlock()
   132  
   133  	out, err := f.getWriterLocked(false, false)
   134  	if err != nil {
   135  		return 0, fmt.Errorf("acquite rotated file :%w", err)
   136  	}
   137  
   138  	return out.Write(b)
   139  }
   140  
   141  // WriteString is like Write, but writes the contents of string s rather than
   142  // a slice of bytes.
   143  func (f *RotateFile) WriteString(s string) (n int, err error) {
   144  	if err := f.checkValid("close"); err != nil {
   145  		return 0, err
   146  	}
   147  	return f.Write([]byte(s))
   148  }
   149  
   150  // WriteAt writes len(b) bytes to the File starting at byte offset off.
   151  // It returns the number of bytes written and an error, if any.
   152  // WriteAt returns a non-nil error when n != len(b).
   153  //
   154  // If file was opened with the O_APPEND flag, WriteAt returns an error.
   155  func (f *RotateFile) WriteAt(b []byte, off int64) (n int, err error) {
   156  	if err := f.checkValid("write"); err != nil {
   157  		return 0, err
   158  	}
   159  	// Guard against concurrent writes
   160  	f.mu.Lock()
   161  	defer f.mu.Unlock()
   162  
   163  	out, err := f.getWriterLocked(false, false)
   164  	if err != nil {
   165  		return 0, fmt.Errorf("acquite rotated file :%w", err)
   166  	}
   167  	if w, ok := out.(io.WriterAt); ok {
   168  		return w.WriteAt(b, off)
   169  	}
   170  	return 0, fmt.Errorf("WriteAt is not supported")
   171  }
   172  
   173  // Close satisfies the io.Closer interface. You must
   174  // call this method if you performed any writes to
   175  // the object.
   176  func (f *RotateFile) Close() error {
   177  	if err := f.checkValid("close"); err != nil {
   178  		return err
   179  	}
   180  	f.mu.Lock()
   181  	defer f.mu.Unlock()
   182  
   183  	if f.usingFile == nil { // maybe file is closed or not open
   184  		return nil
   185  	}
   186  	defer f.serializedClean()
   187  
   188  	defer func() { f.usingFile = nil }()
   189  	return f.usingFile.Close()
   190  }
   191  
   192  // Rotate forcefully rotates the file. If the generated file name
   193  // clash because file already exists, a numeric suffix of the form
   194  // ".1", ".2", ".3" and so forth are appended to the end of the log file
   195  //
   196  // This method can be used in conjunction with a signal handler so to
   197  // emulate servers that generate new log files when they receive a SIGHUP
   198  func (f *RotateFile) Rotate(forceRotate bool) error {
   199  	if err := f.checkValid("rotate"); err != nil {
   200  		return err
   201  	}
   202  	f.mu.Lock()
   203  	defer f.mu.Unlock()
   204  	if _, err := f.getWriterLocked(true, forceRotate); err != nil {
   205  		return err
   206  	}
   207  	return nil
   208  }
   209  
   210  func (f *RotateFile) filePathByRotateTime() string {
   211  	// create a new file name using the regular time layout
   212  	return f.FilePathPrefix + time_.TruncateByLocation(time.Now(), f.RotateInterval).Format(f.FilePathRotateLayout)
   213  }
   214  
   215  func (f *RotateFile) filePathByRotateSize() (name string, seq int) {
   216  	// instead of just using the regular time layout,
   217  	// we create a new file name using names such as "foo.1", "foo.2", "foo.3", etc
   218  	return nextSeqFileName(f.filePathByRotateTime(), f.usingSeq)
   219  }
   220  
   221  func (f *RotateFile) filePathByRotate(forceRotate bool) (name string, seq int, byTime, bySize bool) {
   222  	// name using the regular time layout, without seq
   223  	name = f.filePathByRotateTime()
   224  	// startup
   225  	if f.usingFilePath == "" {
   226  		if f.ForceNewFileOnStartup {
   227  			// instead of just using the regular time layout,
   228  			// we create a new file name using names such as "foo", "foo.1", "foo.2", "foo.3", etc
   229  			name, seq = nextSeqFileName(name, f.usingSeq)
   230  			return name, seq, false, true
   231  		}
   232  		name, seq = maxSeqFileName(name)
   233  		return name, seq, true, false
   234  	}
   235  
   236  	// rotate by time
   237  	// compare expect time with current using file
   238  	if name != trimSeqFromNextFileName(f.usingFilePath, f.usingSeq) {
   239  		if forceRotate {
   240  			// instead of just using the regular time layout,
   241  			// we create a new file name using names such as "foo", "foo.1", "foo.2", "foo.3", etc
   242  			name, seq = nextSeqFileName(name, 0)
   243  			return name, seq, true, false
   244  		}
   245  		name, seq = maxSeqFileName(name)
   246  		return name, seq, true, false
   247  	}
   248  
   249  	// determine if rotate by size
   250  
   251  	// recreate file if file not exist, removed by other process for example
   252  	usingFileInfo, err := os.Stat(f.usingFilePath)
   253  	if os.IsNotExist(err) {
   254  		name = f.usingFilePath
   255  		seq = f.usingSeq
   256  		return name, seq, false, false
   257  	}
   258  
   259  	// rotate by size
   260  	// compare rotate size with current using file
   261  	if forceRotate || (err == nil && (f.RotateSize > 0 && usingFileInfo.Size() > f.RotateSize)) {
   262  		// instead of just using the regular time layout,
   263  		// we create a new file name using names such as "foo", "foo.1", "foo.2", "foo.3", etc
   264  		name, seq = nextSeqFileName(name, f.usingSeq)
   265  		return name, seq, false, true
   266  	}
   267  	name = f.usingFilePath
   268  	seq = f.usingSeq
   269  	return name, seq, false, false
   270  }
   271  
   272  func (f *RotateFile) makeUsingFileReadyLocked() error {
   273  	// using file exist, close this file if not ready to use
   274  	if f.usingFile != nil {
   275  		_, err := os.Stat(f.usingFile.Name())
   276  		if err == nil {
   277  			return nil
   278  		}
   279  		_ = f.usingFile.Close()
   280  		f.usingFile = nil
   281  	}
   282  
   283  	// using file not exist, recreate the file and file link
   284  	file, err := AppendAllIfNotExist(f.usingFilePath)
   285  	if err != nil {
   286  		return err
   287  	}
   288  
   289  	// link -> filename
   290  	if f.FileLinkPath != "" {
   291  		if err := ReSymlink(f.usingFilePath, f.FileLinkPath); err != nil {
   292  			return err
   293  		}
   294  	}
   295  	f.usingFile = file
   296  	return nil
   297  
   298  }
   299  
   300  func (f *RotateFile) getWriterLocked(bailOnRotateFail, forceRotate bool) (out io.Writer, err error) {
   301  	newName, newSeq, byTime, bySize := f.filePathByRotate(forceRotate)
   302  	if !byTime && !bySize { // no need to rotate by time and size
   303  		err = f.makeUsingFileReadyLocked()
   304  		if err != nil {
   305  			return nil, err
   306  		}
   307  		return f.usingFile, nil
   308  	}
   309  
   310  	// rotate the file
   311  	if f.PreRotateHandler != nil {
   312  		f.PreRotateHandler(f.usingFilePath)
   313  	}
   314  	newFile, err := f.rotateLocked(newName)
   315  	if err != nil {
   316  		if bailOnRotateFail {
   317  			// Failure to rotate is a problem, but it's really not a great idea
   318  			// to stop your application just because you couldn't rename your log.
   319  			//
   320  			// We only return this error when explicitly needed (as specified by bailOnRotateFail)
   321  			return nil, err
   322  		}
   323  		// no file can be written, it's an error explicitly
   324  		if f.usingFile == nil {
   325  			return nil, err
   326  		}
   327  		return f.usingFile, nil
   328  	}
   329  
   330  	// swap file
   331  	if f.usingFile != nil {
   332  		_ = f.usingFile.Close()
   333  		f.usingFile = nil
   334  	}
   335  	f.usingFile = newFile
   336  	f.usingFilePath = newName
   337  	f.usingSeq = newSeq
   338  	if f.PostRotateHandler != nil {
   339  		f.PostRotateHandler(f.usingFilePath)
   340  	}
   341  
   342  	return f.usingFile, nil
   343  }
   344  
   345  // file may not be nil if err is nil
   346  func (f *RotateFile) rotateLocked(newName string) (*os.File, error) {
   347  	// if we got here, then we need to create a file
   348  	if newName != f.usingFilePath {
   349  		var err error
   350  		var mode string
   351  		switch f.RotateMode {
   352  		case RotateModeCopyRename:
   353  			// for which open the file, and write file not by RotateFile
   354  			// CopyRenameFileAll = RenameFileAll(src->dst) + OpenFile(src)
   355  			// usingFilePath -> newName + recreate usingFilePath
   356  			err = CopyRenameAll(newName, f.usingFilePath)
   357  			mode = "copy_rename"
   358  		case RotateModeCopyTruncate:
   359  			// for which open the file, and write file not by RotateFile
   360  			// CopyTruncateFile = CopyFile(src->dst) + Truncate(src)
   361  			// usingFilePath -> newName + truncate usingFilePath
   362  			err = CopyTruncateAll(newName, f.usingFilePath)
   363  			mode = "copy_truncate"
   364  		case RotateModeNew:
   365  			// for which open the file, and write file by RotateFile
   366  			mode = "new"
   367  			fallthrough
   368  		default:
   369  		}
   370  		if err != nil {
   371  			return nil, fmt.Errorf("failed to %s file %s to %s: %w", mode, f.usingFilePath, newName, err)
   372  		}
   373  	}
   374  
   375  	// make sure new file exists
   376  	file, err := AppendAllIfNotExist(newName)
   377  	if err != nil {
   378  		return nil, err
   379  	}
   380  
   381  	// link -> filename
   382  	if f.FileLinkPath != "" {
   383  		if err := ReSymlink(newName, f.FileLinkPath); err != nil {
   384  			return nil, err
   385  		}
   386  	}
   387  	// unlink files on a separate goroutine
   388  	go f.serializedClean()
   389  
   390  	return file, nil
   391  }
   392  
   393  // unlink files
   394  // expect run on a separate goroutine
   395  func (f *RotateFile) serializedClean() error {
   396  	// running already, ignore duplicate clean
   397  	if !f.cleaning.CAS(false, true) {
   398  		return nil
   399  	}
   400  	defer f.cleaning.Store(false)
   401  
   402  	now := time.Now()
   403  
   404  	// find old files
   405  	var filesNotExpired []string
   406  	filesExpired, err := filepath_.GlobFunc(f.FilePathPrefix+f.RotateFileGlob, func(name string) bool {
   407  		fi, err := os.Stat(name)
   408  		if err != nil {
   409  			return false
   410  		}
   411  
   412  		fl, err := os.Lstat(name)
   413  		if err != nil {
   414  			return false
   415  		}
   416  		if f.MaxAge <= 0 {
   417  			filesNotExpired = append(filesNotExpired, name)
   418  			return false
   419  		}
   420  
   421  		if now.Sub(fi.ModTime()) < f.MaxAge {
   422  			filesNotExpired = append(filesNotExpired, name)
   423  			return false
   424  		}
   425  
   426  		if fl.Mode()&os.ModeSymlink == os.ModeSymlink {
   427  			return false
   428  		}
   429  		return true
   430  	})
   431  	if err != nil {
   432  		return err
   433  	}
   434  
   435  	var filesExceedMaxCount []string
   436  	if f.MaxCount > 0 && len(filesNotExpired) > 0 {
   437  		removeCount := len(filesNotExpired) - f.MaxCount
   438  		if removeCount < 0 {
   439  			removeCount = 0
   440  		}
   441  		sort.Sort(rotateFileSlice(filesNotExpired))
   442  		filesExceedMaxCount = filesNotExpired[:removeCount]
   443  	}
   444  	var errs []error
   445  	for _, path := range filesExpired {
   446  		err = os.Remove(path)
   447  		if err != nil {
   448  			errs = append(errs, err)
   449  		}
   450  	}
   451  	for _, path := range filesExceedMaxCount {
   452  		err = os.Remove(path)
   453  		if err != nil {
   454  			errs = append(errs, err)
   455  		}
   456  	}
   457  	return errors.Join(errs...)
   458  }
   459  
   460  // checkValid checks whether f is valid for use.
   461  // If not, it returns an appropriate error, perhaps incorporating the operation name op.
   462  func (f *RotateFile) checkValid(op string) error {
   463  	if f == nil {
   464  		return os.ErrInvalid
   465  	}
   466  	return nil
   467  }
   468  
   469  // foo.txt, 0 -> foo.txt
   470  // foo.txt, 1 -> foo.txt.[1,2,...], which is not exist and seq is max
   471  func nextSeqFileName(name string, seq int) (string, int) {
   472  	// Special case: if seq is 0, we don't want to append 0 to the file name.
   473  	if seq == 0 {
   474  		nf, err := LockAll(name)
   475  		if !os.IsExist(err) {
   476  			return name, seq
   477  		}
   478  		_ = nf.Close()
   479  		seq++
   480  	}
   481  
   482  	// A new file has been requested. Instead of just using the
   483  	// regular strftime pattern, we create a new file name using
   484  	// generational names such as "foo.1", "foo.2", "foo.3", etc
   485  	nf, seqUsed, err := NextFile(name+".*", seq)
   486  	if err != nil {
   487  		return name, seq
   488  	}
   489  	defer nf.Close()
   490  	if seqUsed == 0 {
   491  		return name, seqUsed
   492  	}
   493  	return nf.Name(), seqUsed
   494  }
   495  
   496  // foo.txt -> foo.txt
   497  // foo.txt.1 -> foo.txt
   498  // foo.txt.1.1 -> foo.txt.1
   499  func trimSeqFromNextFileName(name string, seq int) string {
   500  	if seq == 0 {
   501  		return name
   502  	}
   503  	return strings.TrimSuffix(name, fmt.Sprintf(".%d", seq))
   504  }
   505  
   506  // foo.txt.* -> foo.txt.[1,2,...], which exists and seq is max
   507  func maxSeqFileName(name string) (string, int) {
   508  	prefix, seq, suffix := MaxSeq(name + ".*")
   509  	if seq == 0 {
   510  		return name, seq
   511  	}
   512  	return fmt.Sprintf("%s%d%s", prefix, seq, suffix), seq
   513  }
   514  
   515  // sort filename by mode time and ascii in increase order
   516  type rotateFileSlice []string
   517  
   518  func (s rotateFileSlice) Len() int {
   519  	return len(s)
   520  }
   521  func (s rotateFileSlice) Swap(i, j int) {
   522  	s[i], s[j] = s[j], s[i]
   523  }
   524  
   525  func (s rotateFileSlice) Less(i, j int) bool {
   526  	fi, err := os.Stat(s[i])
   527  	if err != nil {
   528  		return false
   529  	}
   530  	fj, err := os.Stat(s[j])
   531  	if err != nil {
   532  		return false
   533  	}
   534  	if fi.ModTime().Equal(fj.ModTime()) {
   535  		if len(s[i]) == len(s[j]) {
   536  			return s[i] < s[j]
   537  		}
   538  		return len(s[i]) > len(s[j]) // foo.1, foo.2, ..., foo
   539  	}
   540  	return fi.ModTime().Before(fj.ModTime())
   541  }