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) } }