package main import ( "os" "path/filepath" "regexp" "strconv" "strings" "time" logger "git.site.quack-lab.dev/dave/cylogger" "github.com/bmatcuk/doublestar/v4" ) type Config struct { WorkDir 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() workdir := filepath.ToSlash(strings.TrimSpace(getenv("WORKDIR", "/tmp"))) if pfx := strings.TrimSpace(getenv("PATH_PREFIX", "")); pfx != "" { workdir = filepath.ToSlash(filepath.Join(workdir, 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(" WORKDIR: %s", workdir) logger.Info(" PATH_PREFIX: %s", getenv("PATH_PREFIX", "")) logger.Info(" FORBIDDEN: %v", patterns) logger.Info(" SCAN_INTERVAL(ms): %d", interval.Milliseconds()) return Config{ WorkDir: workdir, Patterns: patterns, ScanInterval: interval, } } func deleteMatches(cfg Config) { log := logger.Default.WithPrefix("deleteMatches") if cfg.WorkDir == "" { log.Error("WorkDir is empty") return } if _, err := os.Stat(cfg.WorkDir); err != nil { log.Error("WorkDir not accessible %s: %v", cfg.WorkDir, err) return } for _, pat := range cfg.Patterns { if pat == "" { continue } matches, err := doublestar.Glob(os.DirFS(cfg.WorkDir), pat) if err != nil { log.Error("glob %q: %v", pat, err) continue } if len(matches) == 0 { log.Debug("No matches for pattern %q", pat) continue } for _, rel := range matches { full := filepath.Clean(filepath.Join(cfg.WorkDir, rel)) info, err := os.Stat(full) if err != nil { log.Warning("stat %s: %v", full, err) continue } if info.IsDir() { log.Info("Removing directory %s", full) } else { log.Info("Removing file %s", full) } if err := os.RemoveAll(full); err != nil { log.Error("remove %s: %v", full, err) continue } } } } func doRun(cfg Config) { deleteMatches(cfg) } func main() { cfg := loadConfig() logger.Info("Starting forbidden cleaner") doRun(cfg) t := time.NewTicker(cfg.ScanInterval) defer t.Stop() for { select { case ts := <-t.C: logger.Info("Tick %d", ts.UnixMilli()) doRun(cfg) } } }