113 lines
2.8 KiB
Go
113 lines
2.8 KiB
Go
package splinter
|
|
|
|
import (
|
|
"compress/gzip"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"time"
|
|
)
|
|
|
|
const rotationTimestampFormat = "20060102T150405.000000000Z"
|
|
|
|
// shouldRotate reports whether the current file has hit a rotation trigger.
|
|
// Caller must hold s.mu.
|
|
func (s *FileStream) shouldRotate() bool {
|
|
if s.cfg.MaxSizeMB > 0 && s.counter.n >= int64(s.cfg.MaxSizeMB)*1024*1024 {
|
|
return true
|
|
}
|
|
if s.cfg.MaxAge > 0 && time.Since(s.openedAt) >= s.cfg.MaxAge {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// rotate closes the current file, renames it to a timestamped backup, opens
|
|
// a fresh file, and kicks off async compression + pruning. Caller holds s.mu.
|
|
func (s *FileStream) rotate() error {
|
|
if err := s.file.Close(); err != nil {
|
|
return err
|
|
}
|
|
|
|
ts := time.Now().UTC().Format(rotationTimestampFormat)
|
|
ext := filepath.Ext(s.path)
|
|
base := s.path[:len(s.path)-len(ext)]
|
|
backupPath := fmt.Sprintf("%s.%s%s", base, ts, ext)
|
|
|
|
if err := os.Rename(s.path, backupPath); err != nil {
|
|
return err
|
|
}
|
|
|
|
f, err := os.OpenFile(s.path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.attach(f, 0)
|
|
s.openedAt = time.Now()
|
|
|
|
go s.compressAndPrune(backupPath, base, ext)
|
|
return nil
|
|
}
|
|
|
|
// compressAndPrune runs in the background after rotation: gzips the new
|
|
// backup (if configured) and deletes any stale backups beyond MaxBackups.
|
|
// Errors are swallowed since this is best-effort housekeeping.
|
|
func (s *FileStream) compressAndPrune(backupPath, base, ext string) {
|
|
if s.cfg.Compress {
|
|
_ = gzipFile(backupPath)
|
|
}
|
|
s.pruneBackups(base, ext)
|
|
}
|
|
|
|
// gzipFile compresses src to src+".gz", then removes src on success.
|
|
func gzipFile(src string) error {
|
|
in, err := os.Open(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer in.Close()
|
|
|
|
out, err := os.OpenFile(src+".gz", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
gz := gzip.NewWriter(out)
|
|
|
|
if _, err := io.Copy(gz, in); err != nil {
|
|
gz.Close()
|
|
out.Close()
|
|
os.Remove(src + ".gz")
|
|
return err
|
|
}
|
|
if err := gz.Close(); err != nil {
|
|
out.Close()
|
|
os.Remove(src + ".gz")
|
|
return err
|
|
}
|
|
if err := out.Close(); err != nil {
|
|
os.Remove(src + ".gz")
|
|
return err
|
|
}
|
|
return os.Remove(src)
|
|
}
|
|
|
|
// pruneBackups removes the oldest rotated files when the total exceeds
|
|
// MaxBackups. Both raw and gzipped backups are considered.
|
|
func (s *FileStream) pruneBackups(base, ext string) {
|
|
raw, _ := filepath.Glob(fmt.Sprintf("%s.*%s", base, ext))
|
|
gz, _ := filepath.Glob(fmt.Sprintf("%s.*%s.gz", base, ext))
|
|
|
|
all := append(raw, gz...)
|
|
if len(all) <= s.cfg.MaxBackups {
|
|
return
|
|
}
|
|
// Lex sort puts older timestamps first; trailing ".gz" sorts after the
|
|
// raw form, so any transient duplicate during compression is handled
|
|
// naturally on the next rotation.
|
|
sort.Strings(all)
|
|
for _, p := range all[:len(all)-s.cfg.MaxBackups] {
|
|
_ = os.Remove(p)
|
|
}
|
|
}
|