package main import ( "log" "os" "path/filepath" "sync" "time" "github.com/robfig/cron/v3" ) // ----------------------------------------------------------------------------- // TTL 清理器 // ----------------------------------------------------------------------------- var cleanerOnce sync.Once func startCleaner() { cleanerOnce.Do(func() { if err := os.MkdirAll(*uploadDir, 0o755); err != nil { log.Fatalf("create upload dir failed: %v", err) } c := cron.New() _, err := c.AddFunc("@every "+scanEvery.String(), func() { now := time.Now() removed := 0 var removedBytes int64 _ = filepath.Walk(*uploadDir, func(path string, info os.FileInfo, err error) error { if err != nil || info.IsDir() { return nil } if now.Sub(info.ModTime()) > *fileTTL { if rmErr := os.Remove(path); rmErr != nil { log.Printf("[cleaner] remove failed: %s err=%v", path, rmErr) } else { log.Printf("[cleaner] removed: %s (age=%v, size=%s)", path, now.Sub(info.ModTime()), formatSize(info.Size())) removed++ removedBytes += info.Size() } } return nil }) quotaSub(removedBytes) // 漂移修正: 每小时一次 rescan 校正任何来源不明的字节数偏差 rescanQuota() log.Printf("[cleaner] scan done, removed=%d (%s), quota used now=%s", removed, formatSize(removedBytes), formatSize(quotaUsedSnapshot())) }) if err != nil { log.Fatalf("add cron job failed: %v", err) } c.Start() // 分片目录独立清理: 复用同一周期, 清掉孤儿分片并释放预留配额 go func() { cleanupChunks(*fileTTL) // 启动先清一轮, 处理上次进程遗留 ticker := time.NewTicker(*scanEvery) defer ticker.Stop() for range ticker.C { cleanupChunks(*fileTTL) } }() log.Printf("[cleaner] started, schedule=@every %s dir=%s ttl=%s", scanEvery, *uploadDir, *fileTTL) }) }