Files
backrest/internal/rotatinglog/rotatinglog.go
2024-02-04 02:50:53 -08:00

219 lines
4.3 KiB
Go

package rotatinglog
import (
"archive/tar"
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path"
"slices"
"sort"
"strconv"
"strings"
"sync"
"time"
"go.uber.org/zap"
)
var ErrFileNotFound = errors.New("file not found")
var ErrNotFound = errors.New("entry not found")
var ErrBadName = errors.New("bad name")
type RotatingLog struct {
mu sync.Mutex
dir string
lastFile string
maxLogFiles int
now func() time.Time
}
func NewRotatingLog(dir string, maxLogFiles int) *RotatingLog {
return &RotatingLog{dir: dir, maxLogFiles: maxLogFiles}
}
func (r *RotatingLog) curfile() string {
t := time.Now()
if r.now != nil {
t = r.now() // for testing
}
return path.Join(r.dir, t.Format("2006-01-02-logs.tar"))
}
func (r *RotatingLog) removeExpiredFiles() error {
if r.maxLogFiles < 0 {
return nil
}
files, err := r.files()
if err != nil {
return fmt.Errorf("list files: %w", err)
}
if len(files) >= r.maxLogFiles {
for i := 0; i < len(files)-r.maxLogFiles+1; i++ {
if err := os.Remove(path.Join(r.dir, files[i])); err != nil {
return err
}
}
}
return nil
}
func (r *RotatingLog) Write(data []byte) (string, error) {
r.mu.Lock()
defer r.mu.Unlock()
data, err := compress(data)
if err != nil {
return "", err
}
file := r.curfile()
if file != r.lastFile {
if err := os.MkdirAll(r.dir, os.ModePerm); err != nil {
return "", err
}
r.lastFile = file
if err := r.removeExpiredFiles(); err != nil {
zap.L().Error("failed to remove expired files for rotatinglog", zap.Error(err), zap.String("dir", r.dir))
}
}
f, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE, os.ModePerm)
if err != nil {
return "", err
}
defer f.Close()
size, err := f.Seek(0, io.SeekEnd)
if err != nil {
return "", err
}
pos := int64(0)
if size != 0 {
pos, err = f.Seek(-1024, io.SeekEnd)
if err != nil {
return "", err
}
}
tw := tar.NewWriter(f)
defer tw.Close()
tw.WriteHeader(&tar.Header{
Name: fmt.Sprintf("%d.gz", pos),
Size: int64(len(data)),
Mode: 0600,
Typeflag: tar.TypeReg,
ModTime: time.Now(),
})
_, err = tw.Write(data)
if err != nil {
return "", err
}
return fmt.Sprintf("%s/%d", path.Base(file), pos), nil
}
func (r *RotatingLog) Read(name string) ([]byte, error) {
r.mu.Lock()
defer r.mu.Unlock()
// parse name e.g. of the form "2006-01-02-15-04-05.tar/1234"
splitAt := strings.Index(name, "/")
if splitAt == -1 {
return nil, ErrBadName
}
offset, err := strconv.Atoi(name[splitAt+1:])
if err != nil {
return nil, ErrBadName
}
// open file and seek to the offset where the tarball segment should start
f, err := os.Open(path.Join(r.dir, name[:splitAt]))
if err != nil {
if os.IsNotExist(err) {
return nil, ErrFileNotFound
}
return nil, fmt.Errorf("open failed: %w", err)
}
defer f.Close()
f.Seek(int64(offset), io.SeekStart)
// search for the tarball segment in the tarball and read + decompress it if found
seekName := fmt.Sprintf("%d.gz", offset)
tr := tar.NewReader(f)
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, fmt.Errorf("next failed: %v", err)
}
if hdr.Name == seekName {
buf := make([]byte, hdr.Size)
_, err := io.ReadFull(tr, buf)
if err != nil {
return nil, fmt.Errorf("read failed: %v", err)
}
return decompress(buf)
}
}
return nil, ErrNotFound
}
func (r *RotatingLog) files() ([]string, error) {
files, err := os.ReadDir(r.dir)
if err != nil {
return nil, err
}
files = slices.DeleteFunc(files, func(f fs.DirEntry) bool {
return f.IsDir() || !strings.HasSuffix(f.Name(), "-logs.tar")
})
sort.Slice(files, func(i, j int) bool {
return files[i].Name() < files[j].Name()
})
var result []string
for _, f := range files {
result = append(result, f.Name())
}
return result, nil
}
func compress(data []byte) ([]byte, error) {
var buf bytes.Buffer
zw := gzip.NewWriter(&buf)
if _, err := zw.Write(data); err != nil {
return nil, err
}
if err := zw.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func decompress(compressedData []byte) ([]byte, error) {
var buf bytes.Buffer
zr, err := gzip.NewReader(bytes.NewReader(compressedData))
if err != nil {
return nil, err
}
if _, err := io.Copy(&buf, zr); err != nil {
return nil, err
}
if err := zr.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}