- P0-1: subscription_maintenance_queue 使用 RWMutex 防止 channel close/send 竞态 - P0-2: billing_service CalculateCostWithLongContext 修复被吞没的 out-range 错误 - P1-1: timing_wheel_service Schedule/ScheduleRecurring 添加 SetTimer 错误日志 - P1-2: sora_gateway_service StoreFromURLs 失败时降级使用原始 URL - P1-3: concurrency_cache 用 Pipeline 替代 Lua 脚本兼容 Redis Cluster - P1-6: sora_media_cleanup_service runCleanup 添加 nil cfg/storage 防护 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
121 lines
2.8 KiB
Go
121 lines
2.8 KiB
Go
package service
|
|
|
|
import (
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
|
"github.com/robfig/cron/v3"
|
|
)
|
|
|
|
var soraCleanupCronParser = cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
|
|
|
|
// SoraMediaCleanupService 定期清理本地媒体文件
|
|
type SoraMediaCleanupService struct {
|
|
storage *SoraMediaStorage
|
|
cfg *config.Config
|
|
|
|
cron *cron.Cron
|
|
|
|
startOnce sync.Once
|
|
stopOnce sync.Once
|
|
}
|
|
|
|
func NewSoraMediaCleanupService(storage *SoraMediaStorage, cfg *config.Config) *SoraMediaCleanupService {
|
|
return &SoraMediaCleanupService{
|
|
storage: storage,
|
|
cfg: cfg,
|
|
}
|
|
}
|
|
|
|
func (s *SoraMediaCleanupService) Start() {
|
|
if s == nil || s.cfg == nil {
|
|
return
|
|
}
|
|
if !s.cfg.Sora.Storage.Cleanup.Enabled {
|
|
log.Printf("[SoraCleanup] not started (disabled)")
|
|
return
|
|
}
|
|
if s.storage == nil || !s.storage.Enabled() {
|
|
log.Printf("[SoraCleanup] not started (storage disabled)")
|
|
return
|
|
}
|
|
|
|
s.startOnce.Do(func() {
|
|
schedule := strings.TrimSpace(s.cfg.Sora.Storage.Cleanup.Schedule)
|
|
if schedule == "" {
|
|
log.Printf("[SoraCleanup] not started (empty schedule)")
|
|
return
|
|
}
|
|
loc := time.Local
|
|
if strings.TrimSpace(s.cfg.Timezone) != "" {
|
|
if parsed, err := time.LoadLocation(strings.TrimSpace(s.cfg.Timezone)); err == nil && parsed != nil {
|
|
loc = parsed
|
|
}
|
|
}
|
|
c := cron.New(cron.WithParser(soraCleanupCronParser), cron.WithLocation(loc))
|
|
if _, err := c.AddFunc(schedule, func() { s.runCleanup() }); err != nil {
|
|
log.Printf("[SoraCleanup] not started (invalid schedule=%q): %v", schedule, err)
|
|
return
|
|
}
|
|
s.cron = c
|
|
s.cron.Start()
|
|
log.Printf("[SoraCleanup] started (schedule=%q tz=%s)", schedule, loc.String())
|
|
})
|
|
}
|
|
|
|
func (s *SoraMediaCleanupService) Stop() {
|
|
if s == nil {
|
|
return
|
|
}
|
|
s.stopOnce.Do(func() {
|
|
if s.cron != nil {
|
|
ctx := s.cron.Stop()
|
|
select {
|
|
case <-ctx.Done():
|
|
case <-time.After(3 * time.Second):
|
|
log.Printf("[SoraCleanup] cron stop timed out")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func (s *SoraMediaCleanupService) runCleanup() {
|
|
if s.cfg == nil || s.storage == nil {
|
|
return
|
|
}
|
|
retention := s.cfg.Sora.Storage.Cleanup.RetentionDays
|
|
if retention <= 0 {
|
|
log.Printf("[SoraCleanup] skipped (retention_days=%d)", retention)
|
|
return
|
|
}
|
|
cutoff := time.Now().AddDate(0, 0, -retention)
|
|
deleted := 0
|
|
|
|
roots := []string{s.storage.ImageRoot(), s.storage.VideoRoot()}
|
|
for _, root := range roots {
|
|
if root == "" {
|
|
continue
|
|
}
|
|
_ = filepath.Walk(root, func(p string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
if info.IsDir() {
|
|
return nil
|
|
}
|
|
if info.ModTime().Before(cutoff) {
|
|
if rmErr := os.Remove(p); rmErr == nil {
|
|
deleted++
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
log.Printf("[SoraCleanup] cleanup finished, deleted=%d", deleted)
|
|
}
|