refactor: replace CLI flags with env-based config, add flexible duration
parsing, and use ScanInterval ticker to schedule deletions
This commit is contained in:
115
main.go
115
main.go
@@ -1,9 +1,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -12,11 +13,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
WorkDir string
|
WorkDir string
|
||||||
ScanSeconds int
|
Patterns []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 {
|
func getenv(key, def string) string {
|
||||||
if v, ok := os.LookupEnv(key); ok {
|
if v, ok := os.LookupEnv(key); ok {
|
||||||
return v
|
return v
|
||||||
@@ -24,51 +31,77 @@ func getenv(key, def string) string {
|
|||||||
return def
|
return def
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfig() Config {
|
func parseDurationMS(expr string) int64 {
|
||||||
flagWDir := flag.String("wd", "", "working directory")
|
expr = strings.TrimSpace(expr)
|
||||||
flagScan := flag.Int("scan", 60, "scan interval in seconds")
|
if expr == "" {
|
||||||
logger.InitFlag()
|
return 0
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
workdir := filepath.Clean(*flagWDir)
|
|
||||||
if pfx := strings.TrimSpace(getenv("PATH_PREFIX", "")); pfx != "" {
|
|
||||||
workdir = filepath.Join(workdir, pfx)
|
|
||||||
}
|
}
|
||||||
workdir = filepath.ToSlash(workdir)
|
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
|
||||||
|
}
|
||||||
|
|
||||||
// Gather patterns: args + FORBIDDEN env (comma-separated)
|
func loadConfig() Config {
|
||||||
patterns := []string{}
|
// logger.InitFlag() is optional if you don't use CLI flags to set log level,
|
||||||
patterns = append(patterns, flag.Args()...)
|
// 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 != "" {
|
if env := strings.TrimSpace(getenv("FORBIDDEN", "")); env != "" {
|
||||||
for _, p := range strings.Split(env, ",") {
|
for _, p := range strings.Split(env, ",") {
|
||||||
p = strings.TrimSpace(p)
|
p = strings.TrimSpace(p)
|
||||||
if p != "" {
|
if p != "" {
|
||||||
patterns = append(patterns, p)
|
patterns = append(patterns, filepath.ToSlash(p))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize patterns to slash style; patterns are relative to workdir
|
interval := time.Duration(parseDurationMS(getenv("SCAN_INTERVAL", "60s"))) * time.Millisecond
|
||||||
for i := range patterns {
|
|
||||||
patterns[i] = filepath.ToSlash(strings.TrimSpace(patterns[i]))
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("Config:")
|
logger.Info("Config:")
|
||||||
logger.Info(" WorkDir: %s", workdir)
|
logger.Info(" WORKDIR: %s", workdir)
|
||||||
logger.Info(" ScanSeconds: %d", *flagScan)
|
logger.Info(" PATH_PREFIX: %s", getenv("PATH_PREFIX", ""))
|
||||||
logger.Info(" Patterns: %v", patterns)
|
logger.Info(" FORBIDDEN: %v", patterns)
|
||||||
|
logger.Info(" SCAN_INTERVAL(ms): %d", interval.Milliseconds())
|
||||||
|
|
||||||
return Config{
|
return Config{
|
||||||
WorkDir: workdir,
|
WorkDir: workdir,
|
||||||
ScanSeconds: *flagScan,
|
Patterns: patterns,
|
||||||
Patterns: patterns,
|
ScanInterval: interval,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteMatches(cfg Config) {
|
func deleteMatches(cfg Config) {
|
||||||
log := logger.Default.WithPrefix("deleteMatches")
|
log := logger.Default.WithPrefix("deleteMatches")
|
||||||
|
|
||||||
// Ensure workdir exists
|
|
||||||
if cfg.WorkDir == "" {
|
if cfg.WorkDir == "" {
|
||||||
log.Error("WorkDir is empty")
|
log.Error("WorkDir is empty")
|
||||||
return
|
return
|
||||||
@@ -82,7 +115,6 @@ func deleteMatches(cfg Config) {
|
|||||||
if pat == "" {
|
if pat == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Use doublestar.Glob against the workdir FS; it returns relative paths
|
|
||||||
matches, err := doublestar.Glob(os.DirFS(cfg.WorkDir), pat)
|
matches, err := doublestar.Glob(os.DirFS(cfg.WorkDir), pat)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("glob %q: %v", pat, err)
|
log.Error("glob %q: %v", pat, err)
|
||||||
@@ -94,9 +126,7 @@ func deleteMatches(cfg Config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, rel := range matches {
|
for _, rel := range matches {
|
||||||
full := filepath.Join(cfg.WorkDir, rel)
|
full := filepath.Clean(filepath.Join(cfg.WorkDir, rel))
|
||||||
full = filepath.Clean(full)
|
|
||||||
|
|
||||||
info, err := os.Stat(full)
|
info, err := os.Stat(full)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warning("stat %s: %v", full, err)
|
log.Warning("stat %s: %v", full, err)
|
||||||
@@ -117,18 +147,23 @@ func deleteMatches(cfg Config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func doRun(cfg Config) {
|
||||||
|
deleteMatches(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cfg := loadConfig()
|
cfg := loadConfig()
|
||||||
logger.Info("Starting forbidden cleaner")
|
logger.Info("Starting forbidden cleaner")
|
||||||
|
doRun(cfg)
|
||||||
|
|
||||||
// Run immediately, then on interval
|
t := time.NewTicker(cfg.ScanInterval)
|
||||||
deleteMatches(cfg)
|
defer t.Stop()
|
||||||
|
|
||||||
ticker := time.NewTicker(time.Duration(cfg.ScanSeconds) * time.Second)
|
|
||||||
defer ticker.Stop()
|
|
||||||
for {
|
for {
|
||||||
ts := <-ticker.C
|
select {
|
||||||
logger.Info("Tick %d", ts.UnixMilli())
|
case ts := <-t.C:
|
||||||
deleteMatches(cfg)
|
logger.Info("Tick %d", ts.UnixMilli())
|
||||||
|
doRun(cfg)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user