package cleanup
import (
"os"
"path"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/sirupsen/logrus"
)
type LocalFileCleanup struct {
basePath string
stopCh chan struct{}
done chan struct{}
shutdownOnce sync.Once
cleanupConfig CleanupConfig
}
type CleanupConfig struct {
Enabled bool
Rules []CleanupRule
CleanupHour int
}
type CleanupRule struct {
Name string
Path string
RetentionDays int
Pattern string
Recursive bool
}
func NewLocalFileCleanup(basePath string) *LocalFileCleanup {
cleanup := &LocalFileCleanup{
basePath: basePath,
stopCh: make(chan struct{}),
done: make(chan struct{}),
cleanupConfig: getDefaultConfig(),
}
cleanup.loadConfigFromEnv()
if cleanup.cleanupConfig.Enabled {
go cleanup.start()
logrus.Info("Local file cleanup service started")
} else {
logrus.Info("Local file cleanup service disabled")
close(cleanup.done)
}
return cleanup
}
func getDefaultConfig() CleanupConfig {
return CleanupConfig{
Enabled: true,
CleanupHour: 2,
Rules: []CleanupRule{
{
Name: "cleanup-temp-chunks",
Path: "package_build/temp/chunks",
RetentionDays: 1,
Pattern: "*",
Recursive: true,
},
{
Name: "cleanup-temp-events",
Path: "package_build/temp/events",
RetentionDays: 1,
Pattern: "*",
Recursive: true,
},
{
Name: "cleanup-build-slugs",
Path: "build/tenant",
RetentionDays: 7,
Pattern: "*.tgz",
Recursive: true,
},
{
Name: "cleanup-app-import",
Path: "app/import",
RetentionDays: 7,
Pattern: "*",
Recursive: true,
},
{
Name: "cleanup-app-export",
Path: "app",
RetentionDays: 7,
Pattern: "*.zip",
Recursive: false,
},
{
Name: "cleanup-restore",
Path: "restore",
RetentionDays: 1,
Pattern: "*",
Recursive: true,
},
},
}
}
func (c *LocalFileCleanup) loadConfigFromEnv() {
if enabled := os.Getenv("LOCAL_CLEANUP_ENABLED"); enabled == "false" {
c.cleanupConfig.Enabled = false
return
}
if hourStr := os.Getenv("LOCAL_CLEANUP_HOUR"); hourStr != "" {
if hour, err := strconv.Atoi(hourStr); err == nil && hour >= 0 && hour < 24 {
c.cleanupConfig.CleanupHour = hour
}
}
for i := range c.cleanupConfig.Rules {
rule := &c.cleanupConfig.Rules[i]
envKey := "CLEANUP_RETENTION_" + strings.ToUpper(strings.ReplaceAll(rule.Name, "-", "_"))
if daysStr := os.Getenv(envKey); daysStr != "" {
if days, err := strconv.Atoi(daysStr); err == nil && days > 0 {
logrus.Infof("Override retention for %s: %d days", rule.Name, days)
rule.RetentionDays = days
}
}
}
}
func (c *LocalFileCleanup) start() {
defer close(c.done)
logrus.Infof("Local file cleanup scheduled at %02d:00 daily", c.cleanupConfig.CleanupHour)
c.runCleanup()
now := time.Now()
next := time.Date(now.Year(), now.Month(), now.Day(), c.cleanupConfig.CleanupHour, 0, 0, 0, now.Location())
if now.Hour() >= c.cleanupConfig.CleanupHour {
next = next.AddDate(0, 0, 1)
}
firstDelay := next.Sub(now)
logrus.Infof("First cleanup will run in %v at %s", firstDelay.Round(time.Minute), next.Format("2006-01-02 15:04:05"))
firstTimer := time.NewTimer(firstDelay)
defer firstTimer.Stop()
select {
case <-firstTimer.C:
c.runCleanup()
case <-c.stopCh:
logrus.Info("Local file cleanup stopped before first run")
return
}
ticker := time.NewTicker(24 * time.Hour)
defer ticker.Stop()
for {
select {
case <-ticker.C:
c.runCleanup()
case <-c.stopCh:
logrus.Info("Local file cleanup stopped")
return
}
}
}
func (c *LocalFileCleanup) runCleanup() {
logrus.Info("====== Starting local file cleanup ======")
startTime := time.Now()
totalDeleted := 0
totalSize := int64(0)
totalErrors := 0
for _, rule := range c.cleanupConfig.Rules {
deleted, size, errors := c.cleanupByRule(rule)
totalDeleted += deleted
totalSize += size
totalErrors += errors
}
duration := time.Since(startTime)
logrus.Infof("====== Local file cleanup completed in %v ======", duration.Round(time.Second))
logrus.Infof("Summary: deleted %d files (%.2f MB), %d errors",
totalDeleted,
float64(totalSize)/(1024*1024),
totalErrors)
}
func (c *LocalFileCleanup) cleanupByRule(rule CleanupRule) (deleted int, size int64, errors int) {
dirPath := filepath.Join(c.basePath, rule.Path)
if _, err := os.Stat(dirPath); os.IsNotExist(err) {
logrus.Debugf("[%s] Directory does not exist: %s", rule.Name, dirPath)
return 0, 0, 0
}
logrus.Infof("[%s] Cleaning files older than %d days in %s (pattern: %s)",
rule.Name, rule.RetentionDays, dirPath, rule.Pattern)
cutoffTime := time.Now().AddDate(0, 0, -rule.RetentionDays)
if rule.Recursive {
deleted, size, errors = c.cleanupRecursive(dirPath, rule.Pattern, cutoffTime, rule.Name)
} else {
deleted, size, errors = c.cleanupDirectory(dirPath, rule.Pattern, cutoffTime, rule.Name)
}
if deleted > 0 || errors > 0 {
logrus.Infof("[%s] Cleaned %d files (%.2f MB), %d errors",
rule.Name, deleted, float64(size)/(1024*1024), errors)
} else {
logrus.Debugf("[%s] No old files to clean", rule.Name)
}
return deleted, size, errors
}
func (c *LocalFileCleanup) cleanupRecursive(dirPath, pattern string, cutoffTime time.Time, ruleName string) (deleted int, size int64, errors int) {
err := filepath.Walk(dirPath, func(filePath string, info os.FileInfo, err error) error {
if err != nil {
logrus.Warnf("[%s] Error accessing %s: %v", ruleName, filePath, err)
errors++
return nil
}
if info.IsDir() {
return nil
}
matched, err := filepath.Match(pattern, filepath.Base(filePath))
if err != nil {
logrus.Warnf("[%s] Invalid pattern %s: %v", ruleName, pattern, err)
return nil
}
if !matched {
return nil
}
if info.ModTime().Before(cutoffTime) {
if err := os.Remove(filePath); err != nil {
logrus.Errorf("[%s] Failed to delete %s: %v", ruleName, filePath, err)
errors++
} else {
deleted++
size += info.Size()
logrus.Debugf("[%s] Deleted: %s (age: %v, size: %d bytes)",
ruleName,
filePath,
time.Since(info.ModTime()).Round(time.Hour),
info.Size())
}
}
return nil
})
if err != nil {
logrus.Errorf("[%s] Walk error: %v", ruleName, err)
errors++
}
return deleted, size, errors
}
func (c *LocalFileCleanup) cleanupDirectory(dirPath, pattern string, cutoffTime time.Time, ruleName string) (deleted int, size int64, errors int) {
entries, err := os.ReadDir(dirPath)
if err != nil {
logrus.Errorf("[%s] Failed to read directory %s: %v", ruleName, dirPath, err)
return 0, 0, 1
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
matched, err := filepath.Match(pattern, entry.Name())
if err != nil {
logrus.Warnf("[%s] Invalid pattern %s: %v", ruleName, pattern, err)
continue
}
if !matched {
continue
}
filePath := path.Join(dirPath, entry.Name())
fileInfo, err := entry.Info()
if err != nil {
logrus.Warnf("[%s] Failed to get file info for %s: %v", ruleName, filePath, err)
errors++
continue
}
if fileInfo.ModTime().Before(cutoffTime) {
if err := os.Remove(filePath); err != nil {
logrus.Errorf("[%s] Failed to delete %s: %v", ruleName, filePath, err)
errors++
} else {
deleted++
size += fileInfo.Size()
logrus.Debugf("[%s] Deleted: %s (age: %v, size: %d bytes)",
ruleName,
entry.Name(),
time.Since(fileInfo.ModTime()).Round(time.Hour),
fileInfo.Size())
}
}
}
return deleted, size, errors
}
func (c *LocalFileCleanup) Close() error {
c.shutdownOnce.Do(func() {
if c.cleanupConfig.Enabled {
logrus.Info("Stopping local file cleanup service...")
close(c.stopCh)
<-c.done
logrus.Info("Local file cleanup service stopped")
}
})
return nil
}