mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 16:42:15 +01:00
236 lines
5.7 KiB
Go
236 lines
5.7 KiB
Go
package gc
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
func init() {
|
|
gotenberg.MustRegisterModule(GarbageCollector{})
|
|
}
|
|
|
|
// GarbageCollector is a module for removing files and directories that have
|
|
// expired. It allows us to make sure that the application does not leak files
|
|
// or directories when running.
|
|
type GarbageCollector struct {
|
|
rootPath string
|
|
graceDuration time.Duration
|
|
excludeSubstr []string
|
|
|
|
ticker *time.Ticker
|
|
done chan bool
|
|
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// GarbageCollectorGraceDurationModifier is a module interface which allows to
|
|
// update the expiration time of files and directories parsed by the garbage
|
|
// collector. For instance, if the grace duration is 30s, the garbage collector
|
|
// will remove paths that have a modification time older than 30s. If there are
|
|
// many GarbageCollectorGraceDurationModifier, only the longest grace duration
|
|
// is selected.
|
|
type GarbageCollectorGraceDurationModifier interface {
|
|
GraceDuration() time.Duration
|
|
}
|
|
|
|
// GarbageCollectorExcludeSubstrModifier is a module interface which adds the
|
|
// given substrings to the exclude list of the garbage collector. If a path
|
|
// contains one of those substrings, the garbage collector ignores it.
|
|
type GarbageCollectorExcludeSubstrModifier interface {
|
|
ExcludeSubstr() []string
|
|
}
|
|
|
|
// Descriptor returns a GarbageCollector's module descriptor.
|
|
func (gc GarbageCollector) Descriptor() gotenberg.ModuleDescriptor {
|
|
return gotenberg.ModuleDescriptor{
|
|
ID: "gc",
|
|
New: func() gotenberg.Module { return new(GarbageCollector) },
|
|
}
|
|
}
|
|
|
|
// Provision sets the module properties.
|
|
func (gc *GarbageCollector) Provision(ctx *gotenberg.Context) error {
|
|
gc.rootPath = gotenberg.TmpPath()
|
|
|
|
graceDurationModifiers, err := ctx.Modules(new(GarbageCollectorGraceDurationModifier))
|
|
if err != nil {
|
|
return fmt.Errorf("get grace duration modifiers: %w", err)
|
|
}
|
|
|
|
for _, graceDurationModifier := range graceDurationModifiers {
|
|
modifier := graceDurationModifier.(GarbageCollectorGraceDurationModifier)
|
|
|
|
if gc.graceDuration < modifier.GraceDuration() {
|
|
gc.graceDuration = modifier.GraceDuration()
|
|
}
|
|
}
|
|
|
|
excludeSubstrModifiers, err := ctx.Modules(new(GarbageCollectorExcludeSubstrModifier))
|
|
if err != nil {
|
|
return fmt.Errorf("get exclude substr modifiers: %w", err)
|
|
}
|
|
|
|
gc.excludeSubstr = strings.Split(os.Getenv("GC_EXCLUDE_SUBSTR"), ",")
|
|
|
|
for _, excludeSubstrModifier := range excludeSubstrModifiers {
|
|
modifier := excludeSubstrModifier.(GarbageCollectorExcludeSubstrModifier)
|
|
|
|
gc.excludeSubstr = append(gc.excludeSubstr, modifier.ExcludeSubstr()...)
|
|
}
|
|
|
|
loggerProvider, err := ctx.Module(new(gotenberg.LoggerProvider))
|
|
if err != nil {
|
|
return fmt.Errorf("get logger provider: %w", err)
|
|
}
|
|
|
|
logger, err := loggerProvider.(gotenberg.LoggerProvider).Logger(gc)
|
|
if err != nil {
|
|
return fmt.Errorf("get logger: %w", err)
|
|
}
|
|
|
|
gc.logger = logger
|
|
|
|
return nil
|
|
}
|
|
|
|
// Start starts the garbage collector.
|
|
func (gc *GarbageCollector) Start() error {
|
|
gc.ticker = time.NewTicker(gc.graceDuration + time.Duration(1)*time.Second)
|
|
gc.done = make(chan bool, 1)
|
|
|
|
go func() {
|
|
for {
|
|
func() {
|
|
gcMu.RLock()
|
|
defer gcMu.RUnlock()
|
|
|
|
select {
|
|
case <-gc.done:
|
|
return
|
|
case <-gc.ticker.C:
|
|
gc.collect(false)
|
|
}
|
|
}()
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
// collect parses the root path of the garbage collector and removes files or
|
|
// directories that have expired. It ignores the expiration date if the "force"
|
|
// argument is set to true.
|
|
func (gc GarbageCollector) collect(force bool) {
|
|
expirationTime := time.Now().Add(-gc.graceDuration)
|
|
|
|
// To make sure that the next Walk method stays on
|
|
// the root level of the considered path, we have to
|
|
// return a filepath.SkipDir error if the current path
|
|
// is a directory.
|
|
skipDirOrNil := func(info os.FileInfo) error {
|
|
if info.IsDir() {
|
|
return filepath.SkipDir
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
removePath := func(path string) {
|
|
err := os.RemoveAll(path)
|
|
if err != nil {
|
|
gc.logger.Error(fmt.Sprintf("remove '%s': %s", path, err))
|
|
}
|
|
|
|
gc.logger.Debug(fmt.Sprintf("'%s' removed", path))
|
|
}
|
|
|
|
err := filepath.Walk(gc.rootPath, func(path string, info os.FileInfo, pathErr error) error {
|
|
if pathErr != nil {
|
|
// For whatever reasons, the Walk method failed
|
|
// to process the current path.
|
|
return pathErr
|
|
}
|
|
|
|
if path == gc.rootPath {
|
|
return nil
|
|
}
|
|
|
|
for _, substr := range gc.excludeSubstr {
|
|
if strings.Contains(info.Name(), substr) {
|
|
return skipDirOrNil(info)
|
|
}
|
|
}
|
|
|
|
if force {
|
|
removePath(path)
|
|
|
|
return skipDirOrNil(info)
|
|
}
|
|
|
|
if info.ModTime().Before(expirationTime) {
|
|
removePath(path)
|
|
}
|
|
|
|
return skipDirOrNil(info)
|
|
})
|
|
|
|
if err != nil {
|
|
gc.logger.Error(err.Error())
|
|
}
|
|
}
|
|
|
|
// StartupMessage returns an empty string.
|
|
func (gc GarbageCollector) StartupMessage() string {
|
|
return ""
|
|
}
|
|
|
|
// Stop stops the garbage collector.
|
|
func (gc *GarbageCollector) Stop(ctx context.Context) error {
|
|
_, ok := ctx.Deadline()
|
|
if !ok {
|
|
return errors.New("no context dead line")
|
|
}
|
|
|
|
// Block until the context is done so that other module may gracefully stop
|
|
// before we do a shutdown cleanup. We skip this step if we receive a
|
|
// SIGINT in the meantime.
|
|
gc.logger.Debug("wait for the end of grace duration")
|
|
|
|
quit := make(chan os.Signal, 1)
|
|
signal.Notify(quit, os.Interrupt)
|
|
|
|
select {
|
|
case <-quit:
|
|
return nil
|
|
case <-ctx.Done():
|
|
break
|
|
}
|
|
|
|
gc.ticker.Stop()
|
|
gc.done <- true
|
|
|
|
gc.logger.Debug("shutdown cleanup...")
|
|
gc.collect(true)
|
|
|
|
return nil
|
|
}
|
|
|
|
var gcMu sync.RWMutex
|
|
|
|
// Interface guards.
|
|
var (
|
|
_ gotenberg.Module = (*GarbageCollector)(nil)
|
|
_ gotenberg.Provisioner = (*GarbageCollector)(nil)
|
|
_ gotenberg.App = (*GarbageCollector)(nil)
|
|
)
|