Files
directory-deletor/main.go

181 lines
4.3 KiB
Go

package main
import (
"flag"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
logger "git.site.quack-lab.dev/dave/cylogger"
"github.com/bmatcuk/doublestar/v4"
)
type Config struct {
Root string
Patterns []string
ScanInterval time.Duration
}
var (
timeUnits = map[string]int64{"ms": 1, "s": 1000, "m": 60_000, "h": 3_600_000, "d": 86_400_000, "M": 2_592_000_000, "y": 31_536_000_000}
valueRegex = regexp.MustCompile(`\d+`)
unitRegex = regexp.MustCompile(`[a-zA-Z]+`)
)
func getenv(key, def string) string {
if v, ok := os.LookupEnv(key); ok {
return v
}
return def
}
func parseDurationMS(expr string) int64 {
expr = strings.TrimSpace(expr)
if expr == "" {
return 0
}
var total int64
for _, p := range strings.Split(expr, "_") {
p = strings.TrimSpace(p)
if p == "" {
continue
}
v := valueRegex.FindString(p)
u := unitRegex.FindString(p)
if v == "" || u == "" {
logger.Warning("Invalid duration part: %q", p)
continue
}
unit, ok := timeUnits[u]
if !ok {
logger.Warning("Invalid duration unit: %q", u)
continue
}
n, err := strconv.ParseInt(v, 10, 64)
if err != nil {
logger.Warning("Invalid duration value: %q: %v", v, err)
continue
}
total += n * unit
}
return total
}
func loadConfig() Config {
// logger.InitFlag() is optional if you don't use CLI flags to set log level,
// but it is safe to call to honor standard logger flags if provided.
logger.InitFlag()
root := filepath.ToSlash(strings.TrimSpace(getenv("ROOT", "/tmp")))
if pfx := strings.TrimSpace(getenv("PATH_PREFIX", "")); pfx != "" {
root = filepath.ToSlash(filepath.Join(root, pfx))
}
// Patterns from FORBIDDEN (comma-separated). Relative to WorkDir.
var patterns []string
if env := strings.TrimSpace(getenv("FORBIDDEN", "")); env != "" {
for _, p := range strings.Split(env, ",") {
p = strings.TrimSpace(p)
if p != "" {
patterns = append(patterns, filepath.ToSlash(p))
}
}
}
interval := time.Duration(parseDurationMS(getenv("SCAN_INTERVAL", "60s"))) * time.Millisecond
logger.Info("Config:")
logger.Info(" ROOT: %s", root)
logger.Info(" PATH_PREFIX: %s", getenv("PATH_PREFIX", ""))
logger.Info(" FORBIDDEN: %v", patterns)
logger.Info(" SCAN_INTERVAL(ms): %d", interval.Milliseconds())
return Config{
Root: root,
Patterns: patterns,
ScanInterval: interval,
}
}
func deleteMatches(cfg Config) {
log := logger.Default.WithPrefix("deleteMatches")
log.Debug("Starting deleteMatches operation.")
log.Trace("Config: %+v", cfg)
if cfg.Root == "" {
log.Error("Root is empty")
return
}
if _, err := os.Stat(cfg.Root); err != nil {
log.Error("Root not accessible %s: %v", cfg.Root, err)
return
}
for _, pat := range cfg.Patterns {
patlog := log.WithPrefix(pat)
patlog.Debug("Processing pattern")
if pat == "" {
continue
}
matches, err := doublestar.Glob(os.DirFS(cfg.Root), pat)
if err != nil {
patlog.Error("glob %q: %v", pat, err)
continue
}
if len(matches) == 0 {
patlog.Debug("No matches for pattern")
continue
}
patlog.Trace("Found matches for pattern %q: %v", pat, matches)
for _, rel := range matches {
itemlog := patlog.WithPrefix(rel)
itemlog.Debug("Processing matched item")
full := filepath.Clean(filepath.Join(cfg.Root, rel))
info, err := os.Stat(full)
if err != nil {
itemlog.Warning("stat %s: %v", full, err)
continue
}
if info.IsDir() {
itemlog.Info("Removing directory %s", full)
} else {
itemlog.Info("Removing file %s", full)
}
itemlog.Trace("Attempting to remove: %s", full)
if err := os.RemoveAll(full); err != nil {
itemlog.Error("remove %s: %v", full, err)
continue
}
itemlog.Debug("Successfully removed")
}
}
log.Debug("Finished deleteMatches operation.")
}
func doRun(cfg Config) {
logger.Debug("Running with config: %+v", cfg)
deleteMatches(cfg)
}
func main() {
flag.Parse()
logger.InitFlag()
cfg := loadConfig()
logger.Info("Starting directory-forbidder")
doRun(cfg)
t := time.NewTicker(cfg.ScanInterval)
defer t.Stop()
for range t.C {
logger.Info("Tick %d", time.Now().UnixMilli())
doRun(cfg)
}
}