Files
scratchbox/cmd/scratchbox/access_log_writer.go
T
s1d3sw1ped ad4355df17
CI / Go Tests (push) Successful in 15s
CI / Build (push) Successful in 9s
Format / gofmt (push) Successful in 7s
Release Artifacts / Validate release tag (push) Successful in 2s
Release Artifacts / Build and release executables (push) Successful in 15s
Release Artifacts / Build and release Docker image (push) Successful in 28s
Heavy security and file splitting
2026-06-01 20:15:28 -05:00

146 lines
3.4 KiB
Go

package main
import (
"fmt"
"io"
"os"
"path/filepath"
"sync"
"scratchbox/internal/config"
)
func newAccessLogWriter(cfg config.Config) (io.Writer, func() error, error) {
if !cfg.Server.AccessLog.Enabled {
return io.Discard, func() error { return nil }, nil
}
if cfg.Server.AccessLog.FilePath == "" {
return os.Stdout, func() error { return nil }, nil
}
writer, err := newRollingFileWriter(
cfg.Server.AccessLog.FilePath,
cfg.Server.AccessLog.MaxSizeBytes,
cfg.Server.AccessLog.MaxBackups,
)
if err != nil {
return nil, nil, err
}
return io.MultiWriter(os.Stdout, writer), writer.Close, nil
}
type rollingFileWriter struct {
mu sync.Mutex
path string
maxSize int64
maxBackups int
file *os.File
size int64
}
func newRollingFileWriter(path string, maxSize int64, maxBackups int) (*rollingFileWriter, error) {
if maxSize <= 0 {
return nil, fmt.Errorf("max size must be > 0")
}
if maxBackups <= 0 {
return nil, fmt.Errorf("max backups must be > 0")
}
if dir := filepath.Dir(path); dir != "" && dir != "." {
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, fmt.Errorf("create log directory: %w", err)
}
}
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return nil, fmt.Errorf("open log file: %w", err)
}
info, err := f.Stat()
if err != nil {
_ = f.Close()
return nil, fmt.Errorf("stat log file: %w", err)
}
return &rollingFileWriter{
path: path,
maxSize: maxSize,
maxBackups: maxBackups,
file: f,
size: info.Size(),
}, nil
}
func (w *rollingFileWriter) Write(p []byte) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()
if w.size > 0 && w.size+int64(len(p)) > w.maxSize {
if err := w.rotateLocked(); err != nil {
return 0, err
}
}
n, err := w.file.Write(p)
w.size += int64(n)
return n, err
}
func (w *rollingFileWriter) Close() error {
w.mu.Lock()
defer w.mu.Unlock()
if w.file == nil {
return nil
}
err := w.file.Close()
w.file = nil
return err
}
func (w *rollingFileWriter) rotateLocked() error {
if err := w.file.Close(); err != nil {
return fmt.Errorf("close before rotate: %w", err)
}
for i := w.maxBackups - 1; i >= 1; i-- {
from := rotatedLogPath(w.path, i)
to := rotatedLogPath(w.path, i+1)
if _, err := os.Stat(from); err != nil {
if os.IsNotExist(err) {
continue
}
return fmt.Errorf("stat rotated file %s: %w", from, err)
}
if err := os.Remove(to); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("remove target backup slot %s: %w", to, err)
}
if err := os.Rename(from, to); err != nil {
return fmt.Errorf("rotate backup %s -> %s: %w", from, to, err)
}
}
firstBackup := rotatedLogPath(w.path, 1)
if err := os.Remove(firstBackup); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("remove first backup %s: %w", firstBackup, err)
}
if _, err := os.Stat(w.path); err == nil {
if err := os.Rename(w.path, firstBackup); err != nil {
return fmt.Errorf("rotate current log to %s: %w", firstBackup, err)
}
} else if !os.IsNotExist(err) {
return fmt.Errorf("stat current log file: %w", err)
}
f, err := os.OpenFile(w.path, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return fmt.Errorf("open rotated log file: %w", err)
}
w.file = f
w.size = 0
return nil
}
func rotatedLogPath(path string, index int) string {
return fmt.Sprintf("%s.%d", path, index)
}