| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441 | 
							- /*
 
-  * Public Domain Software
 
-  *
 
-  * I (Matthias Ladkau) am the author of the source code in this file.
 
-  * I have placed the source code in this file in the public domain.
 
-  *
 
-  * For further information see: http://creativecommons.org/publicdomain/zero/1.0/
 
-  */
 
- package fileutil
 
- import (
 
- 	"fmt"
 
- 	"io/ioutil"
 
- 	"os"
 
- 	"path"
 
- 	"sort"
 
- 	"strconv"
 
- 	"strings"
 
- 	"sync"
 
- 	"devt.de/krotik/common/timeutil"
 
- )
 
- /*
 
- MultiFileBuffer is a file-persitent buffer which can be split over multiple files.
 
- A specified file is opened and used as backend storage for a byte buffer. By
 
- default, the file grows indefinitely. It is possible to specify a rollover
 
- condition to allow the file to rollover once the condition is satisfied.
 
- If the condition is satisfied, the file is closed and a new file is silently
 
- opened for output. The buffer will save old log files by appending the
 
- extensions ‘.1’, ‘.2’ etc., to the file name. The rollover condition is only
 
- checked once at the beginning of a write operation.
 
- For example, with a base file name of app.log, the buffer would create
 
- app.log, app.log.1, app.log.2, etc. The file being written to is always app.log.
 
- When this file is filled, it is closed and renamed to app.log.1, and if files
 
- app.log.1, app.log.2, etc. exist, then they are renamed to app.log.2, app.log.3
 
- etc. respectively.
 
- */
 
- type MultiFileBuffer struct {
 
- 	lock     *sync.Mutex       // Lock for reading and writing
 
- 	filename string            // File name for buffer
 
- 	basename string            // Base file name (file name + iterator decoration)
 
- 	iterator FilenameIterator  // Iterator for file names
 
- 	cond     RolloverCondition // Rollover condition
 
- 	fp       *os.File          // Current file handle
 
- }
 
- /*
 
- NewMultiFileBuffer creates a new MultiFileBuffer with a given file name
 
- iterator and rollover condition.
 
- */
 
- func NewMultiFileBuffer(filename string, it FilenameIterator, cond RolloverCondition) (*MultiFileBuffer, error) {
 
- 	var err error
 
- 	mfb := &MultiFileBuffer{&sync.Mutex{}, filename, it.Basename(filename), it, cond, nil}
 
- 	if err = mfb.checkrollover(); err != nil {
 
- 		return nil, err
 
- 	}
 
- 	if mfb.fp == nil {
 
- 		// File existed and can be continued
 
- 		mfb.lock.Lock()
 
- 		mfb.fp, err = os.OpenFile(mfb.basename, os.O_APPEND|os.O_RDWR, 0660)
 
- 		mfb.lock.Unlock()
 
- 	}
 
- 	return mfb, err
 
- }
 
- /*
 
- Write writes len(p) bytes from p to the underlying data stream. It returns
 
- the number of bytes written from p (0 <= n <= len(p)) and any error
 
- encountered that caused the write to stop early.
 
- */
 
- func (mfb *MultiFileBuffer) Write(output []byte) (int, error) {
 
- 	var b int
 
- 	err := mfb.checkrollover()
 
- 	if err == nil {
 
- 		if mfb.fp == nil {
 
- 			// File existed and can be continued
 
- 			mfb.lock.Lock()
 
- 			mfb.fp, err = os.OpenFile(mfb.basename, os.O_APPEND|os.O_RDWR, 0660)
 
- 			mfb.lock.Unlock()
 
- 		}
 
- 		if err == nil {
 
- 			mfb.lock.Lock()
 
- 			b, err = mfb.fp.Write(output)
 
- 			mfb.lock.Unlock()
 
- 		}
 
- 	}
 
- 	return b, err
 
- }
 
- /*
 
- checkrollover checks if the buffer files should be switched.
 
- */
 
- func (mfb *MultiFileBuffer) checkrollover() error {
 
- 	mfb.lock.Lock()
 
- 	defer mfb.lock.Unlock()
 
- 	//  Update basename here
 
- 	mfb.basename = mfb.iterator.Basename(mfb.filename)
 
- 	// Rollover if the base file does not exist
 
- 	ex, err := PathExists(mfb.basename)
 
- 	if err == nil && (!ex || mfb.cond.CheckRollover(mfb.basename)) {
 
- 		// Rollover if either the base file does not exist or the
 
- 		// rollover condition is satisfied
 
- 		err = mfb.rollover()
 
- 	}
 
- 	return err
 
- }
 
- /*
 
- Close closes the buffer.
 
- */
 
- func (mfb *MultiFileBuffer) Close() error {
 
- 	var err error
 
- 	if mfb.fp != nil {
 
- 		err = mfb.fp.Close()
 
- 		mfb.fp = nil
 
- 	}
 
- 	return err
 
- }
 
- /*
 
- rollover switches the buffer files.
 
- */
 
- func (mfb *MultiFileBuffer) rollover() error {
 
- 	var err error
 
- 	// Recursive file renaming function
 
- 	var ensureFileSlot func(fn string) error
 
- 	ensureFileSlot = func(fn string) error {
 
- 		// Check if the file exists already
 
- 		ex, err := PathExists(fn)
 
- 		if ex && err == nil {
 
- 			// Determine new file name
 
- 			newfn := mfb.iterator.NextName(fn)
 
- 			if newfn == "" {
 
- 				// If it is the end of the iteration just delete the file
 
- 				err = os.Remove(fn)
 
- 			} else {
 
- 				// Ensure the new file name is usable
 
- 				err = ensureFileSlot(newfn)
 
- 				// Rename file according to iterator.NextName()
 
- 				if err == nil {
 
- 					err = os.Rename(fn, newfn)
 
- 				}
 
- 			}
 
- 		}
 
- 		return err
 
- 	}
 
- 	// Close existing file
 
- 	err = mfb.Close()
 
- 	// Create file handle
 
- 	if err == nil {
 
- 		err = ensureFileSlot(mfb.basename)
 
- 		if err == nil {
 
- 			// Overwrite existing base file
 
- 			mfb.fp, err = os.OpenFile(mfb.basename, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0660)
 
- 		}
 
- 	}
 
- 	return err
 
- }
 
- // Rollover conditions
 
- // ===================
 
- /*
 
- RolloverCondition is used by the MultiFileBuffer to check if the buffer files
 
- should be switched.
 
- */
 
- type RolloverCondition interface {
 
- 	/*
 
- 	   CheckRollover checks if the buffer files should be switched.
 
- 	*/
 
- 	CheckRollover(basename string) bool
 
- }
 
- /*
 
- EmptyRolloverCondition creates a rollover condition which is never true.
 
- */
 
- func EmptyRolloverCondition() RolloverCondition {
 
- 	return &emptyRolloverCondition{}
 
- }
 
- /*
 
- emptyRolloverCondition is a rollover condition which is never true.
 
- */
 
- type emptyRolloverCondition struct {
 
- }
 
- /*
 
- NextName returns the next file name based on the current file name.
 
- An empty string means the end of the iteration.
 
- */
 
- func (rc *emptyRolloverCondition) CheckRollover(basename string) bool {
 
- 	return false
 
- }
 
- /*
 
- SizeBasedRolloverCondition creates a new rollover condition based on file
 
- size. The condition is satisfied if the base file exceeds a certain file size.
 
- */
 
- func SizeBasedRolloverCondition(maxSize int64) RolloverCondition {
 
- 	return &sizeBasedRolloverCondition{maxSize}
 
- }
 
- /*
 
- sizeBasedRolloverCondition is the implementation of the size based rollover
 
- condition.
 
- */
 
- type sizeBasedRolloverCondition struct {
 
- 	maxSize int64
 
- }
 
- /*
 
- NextName returns the next file name based on the current file name.
 
- An empty string means the end of the iteration.
 
- */
 
- func (rc *sizeBasedRolloverCondition) CheckRollover(basename string) bool {
 
- 	ret := false
 
- 	if info, err := os.Stat(basename); err == nil {
 
- 		ret = info.Size() >= rc.maxSize
 
- 	}
 
- 	return ret
 
- }
 
- // FilenameIterator
 
- // ================
 
- /*
 
- FilenameIterator is used by the MultiFileBuffer to determine the new file name
 
- when rotating the buffer files. Basename is called before doing any calculation.
 
- This function should do general filename decoration. If the decoration changes
 
- over time then the function needs to also handle the cleanup.
 
- */
 
- type FilenameIterator interface {
 
- 	/*
 
- 		Basename decorades the initial file name.
 
- 	*/
 
- 	Basename(filename string) string
 
- 	/*
 
- 		NextName returns the next file name based on the current file name.
 
- 		An empty string means the end of the iteration.
 
- 	*/
 
- 	NextName(currentName string) string
 
- }
 
- /*
 
- ConsecutiveNumberIterator creates a new file name iterator which adds numbers
 
- at the end of files. Up to maxNum files will be created. A maxNum parameter
 
- < 1 means there is no limit.
 
- */
 
- func ConsecutiveNumberIterator(maxNum int) FilenameIterator {
 
- 	return &consecutiveNumberIterator{maxNum}
 
- }
 
- /*
 
- consecutiveNumberIterator is the implementation of the consecutive number
 
- file iterator.
 
- */
 
- type consecutiveNumberIterator struct {
 
- 	maxNum int
 
- }
 
- /*
 
- Basename decorades the initial file name.
 
- */
 
- func (it *consecutiveNumberIterator) Basename(filename string) string {
 
- 	return filename
 
- }
 
- /*
 
- NextName returns the next file name based on the current file name.
 
- An empty string means the end of the iteration.
 
- */
 
- func (it *consecutiveNumberIterator) NextName(currentName string) string {
 
- 	if i := strings.LastIndex(currentName, "."); i > 0 {
 
- 		if num, err := strconv.ParseInt(currentName[i+1:], 10, 64); err == nil {
 
- 			nextNum := int(num + 1)
 
- 			if it.maxNum > 0 && nextNum > it.maxNum {
 
- 				return ""
 
- 			}
 
- 			return fmt.Sprintf("%s.%v", currentName[:i], nextNum)
 
- 		}
 
- 	}
 
- 	return fmt.Sprintf("%s.1", currentName)
 
- }
 
- /*
 
- DailyDateIterator creates a new file name iterator which adds dates at the
 
- end of files. The log will be switched at least once every day. Up to maxNumPerDay
 
- files will be created per day. A maxNumPerDay parameter < 1 means there is no limit.
 
- Up to maxDays different days will be kept (oldest ones are deleted). A maxDays
 
- parameter < 1 means everything is kept.
 
- */
 
- func DailyDateIterator(maxNumPerDay int, maxDays int) FilenameIterator {
 
- 	return &dailyDateIterator{&consecutiveNumberIterator{maxNumPerDay}, maxDays, timeutil.MakeTimestamp}
 
- }
 
- /*
 
- consecutiveNumberIterator is the implementation of the consecutive number
 
- file iterator.
 
- */
 
- type dailyDateIterator struct {
 
- 	*consecutiveNumberIterator
 
- 	maxDays int
 
- 	tsFunc  func() string // Timestamp function
 
- }
 
- /*
 
- NextName returns the next file name based on the current file name.
 
- An empty string means the end of the iteration.
 
- */
 
- func (it *dailyDateIterator) Basename(filename string) string {
 
- 	// Get todays date
 
- 	ts := it.tsFunc()
 
- 	today, _ := timeutil.TimestampString(ts, "UTC")
 
- 	today = today[:10]
 
- 	// Cleanup old files
 
- 	if it.maxDays > 0 {
 
- 		prefix := path.Base(filename)
 
- 		dir := path.Dir(filename)
 
- 		if files, err := ioutil.ReadDir(dir); err == nil {
 
- 			var datesToConsider []string
 
- 			// Collect all relevant files
 
- 			foundToday := false
 
- 			for _, f := range files {
 
- 				if strings.HasPrefix(f.Name(), prefix) && len(f.Name()) > len(prefix) {
 
- 					dateString := f.Name()[len(prefix)+1:]
 
- 					if !strings.ContainsRune(dateString, '.') {
 
- 						datesToConsider = append(datesToConsider, dateString)
 
- 						if !foundToday {
 
- 							foundToday = dateString == today
 
- 						}
 
- 					}
 
- 				}
 
- 			}
 
- 			// Make sure today is one of the dates
 
- 			if !foundToday {
 
- 				datesToConsider = append(datesToConsider, today)
 
- 			}
 
- 			// Sort them so the newest ones are kept
 
- 			sort.Strings(datesToConsider)
 
- 			//  Check if files need to be removed
 
- 			if len(datesToConsider) > it.maxDays {
 
- 				datesToRemove := datesToConsider[:len(datesToConsider)-it.maxDays]
 
- 				for _, f := range files {
 
- 					for _, dateToRemove := range datesToRemove {
 
- 						if strings.HasPrefix(f.Name(), fmt.Sprintf("%s.%s", prefix, dateToRemove)) {
 
- 							os.Remove(path.Join(dir, f.Name()))
 
- 						}
 
- 					}
 
- 				}
 
- 			}
 
- 		}
 
- 	}
 
- 	return fmt.Sprintf("%s.%s", filename, today)
 
- }
 
 
  |